Skip to content

Commit

Permalink
Initial import of cloud-orchestrator (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
tuommaki authored Jan 19, 2024
1 parent 39f0195 commit 0fc9b7b
Show file tree
Hide file tree
Showing 19 changed files with 4,224 additions and 1 deletion.
17 changes: 17 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: bufbuild/[email protected]
- uses: arduino/setup-protoc@v2

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'

- name: Install protoc-gen-go
run: |
go install github.com/golang/protobuf/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- name: Generating API service protos
run: |
protoc \
./server/api/v1/pb/service.proto \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
--proto_path=.
- name: Build
run: go build -v ./...

Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Generated protobuf files
*.pb.go

# Dependency directories (remove the comment below to include it)
# vendor/
vendor/

# Go workspace file
go.work
204 changes: 204 additions & 0 deletions cloud/orchestrator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package cloud

import (
"errors"
"log"
"os"
"path"

"github.com/gevulotnetwork/cloud-orchestrator/config"
"github.com/gevulotnetwork/cloud-orchestrator/fs"
"github.com/gevulotnetwork/cloud-orchestrator/ops"
)

type Orchestrator struct {
configFactory *config.Factory
}

func NewOrchestrator(cfgFactory *config.Factory) *Orchestrator {
return &Orchestrator{
configFactory: cfgFactory,
}
}

func (o *Orchestrator) programToImageName(program string) string {
// Bucket object name has maximum length of 63 and it requires first letter to
// be a character so adjust the hash.
imgNameLen := min(len(program), 62)
return "a" + program[:imgNameLen]
}

func (o *Orchestrator) PrepareProgramImage(program string, baseImg string) error {
program = o.programToImageName(program)

// Tempdir needed for files from the original base image.
tmp_dir, err := os.MkdirTemp(os.TempDir(), "gevulot")
if err != nil {
return err
}
defer os.RemoveAll(tmp_dir)

reader, err := fs.NewReader(baseImg)
if err != nil {
return err
}

err = copyAll(reader, tmp_dir, "/")
if err != nil {
return err
}

// Generate config for new image.
cfg := o.configFactory.NewConfig(program)

// Add files from the base image to new one.
cfg.Dirs = append(cfg.Dirs, tmp_dir)

// Copy program arguments
cfg.Args = reader.ListArgs()

// Assumption is that there is always at least the arg[0].
if len(cfg.Args) > 0 {
// Forming a program path this way is a best guess effort..
cfg.Program = path.Join(tmp_dir, cfg.Args[0])
cfg.ProgramPath = cfg.Program

// Ensure that program is set executable.
os.Chmod(cfg.Program, 0755)
} else {
return errors.New("base image doesn't have arguments; can't set config.Program")
}

// Create temporary file for the generated image.
f, err := os.Create(path.Join(os.TempDir(), program))
if err != nil {
return err
}
defer f.Close()
defer os.RemoveAll(f.Name())

// Copy env variables. Also configure the nanos kernel version if present.
for k, v := range reader.ListEnv() {
if cfg.Env == nil {
cfg.Env = make(map[string]string)
}

cfg.Env[k] = v

if k == "NANOS_VERSION" {
cfg.NanosVersion = v
}
}

cfg.CloudConfig.ImageName = program
cfg.RunConfig.ImageName = f.Name()

p, ctx, err := ops.Provider(cfg)
if err != nil {
return err
}

imgFile, err := p.BuildImage(ctx)
if err != nil {
return err
}
defer os.RemoveAll(imgFile)

log.Printf("build new image: %q", imgFile)
err = p.CreateImage(ctx, imgFile)
if err != nil {
return err
}

return nil
}

func (o *Orchestrator) CreateInstance(program string) (string, error) {
program = o.programToImageName(program)

cfg := o.configFactory.NewConfig(program)
cfg.CloudConfig.ImageName = program
cfg.RunConfig.InstanceName = program

p, ctx, err := ops.Provider(cfg)
if err != nil {
return "", err
}

// TODO: Figure out if we actually need to pick up the right kernel version
// from the image and configure it per program config?
cfg.RunConfig.Kernel = cfg.Kernel

log.Printf("creating instance on %s\n", cfg.CloudConfig.Platform)
err = p.CreateInstance(ctx)
if err != nil {
return "", err
}
log.Printf("create instance %q on %s\n", cfg.RunConfig.InstanceName, cfg.CloudConfig.Platform)

return cfg.RunConfig.InstanceName, nil
}

func (o *Orchestrator) DeleteInstance(program string) error {
program = o.programToImageName(program)

cfg := o.configFactory.NewConfig(program)
cfg.CloudConfig.ImageName = program
cfg.RunConfig.InstanceName = program

p, ctx, err := ops.Provider(cfg)
if err != nil {
return err
}

log.Printf("deleting instance %q on %s\n", cfg.RunConfig.InstanceName, cfg.CloudConfig.Platform)
err = p.DeleteInstance(ctx, program)
if err != nil {
return err
}
log.Printf("deleted instance %q on %s\n", cfg.RunConfig.InstanceName, cfg.CloudConfig.Platform)

return nil
}

func copyAll(reader *fs.Reader, dstPath string, curPath string) error {
fileEntries, err := reader.ReadDir(curPath)
if err != nil {
return err
}

for _, entry := range fileEntries {
fullSrcPath := path.Join(curPath, entry.Name())
fullDstPath := path.Join(dstPath, entry.Name())

if entry.IsDir() {
err = os.MkdirAll(fullDstPath, 0755)
if err != nil {
return err
}

// Recurse into sub-directories.
err = copyAll(reader, fullDstPath, fullSrcPath)
if err != nil {
return err
}

continue
}

err = reader.CopyFile(fullSrcPath, fullDstPath, true)
if err != nil {
return err
}
}

return nil
}

func min(a, b int) int {
if a < b {
return a
}

return b
}
6 changes: 6 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
platform = "gcp"

[gcp]
bucket = "<bucket name for images>"
project_id = "<gcp project id>"
zone = "<gcp zone>"
115 changes: 115 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package config

import (
"log"
"sync"

"github.com/nanovms/ops/cmd"
api "github.com/nanovms/ops/lepton"
"github.com/nanovms/ops/types"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)

var readOnce sync.Once

func readConfig() {
viper.SetConfigType("toml")
viper.SetConfigName("config")
viper.AddConfigPath("/etc/gevulot")
viper.AddConfigPath("$HOME/.config/gevulot")
viper.AddConfigPath("$HOME/.gevulot")
viper.AddConfigPath(".")
err := viper.ReadInConfig()
if err != nil {
log.Fatal(err)
}
}

func init() {
// Read config only once.
readOnce.Do(readConfig)
}

type Factory struct {
defaults *cmd.MergeConfigContainer
platformConfigurator PlatformConfig
}

// Platform defines the cloud provider, currently supporting aws, azure, and gcp.
func NewFactory() *Factory {
platformCfg, err := getPlatformConfig()
if err != nil {
log.Fatal(err)
}

// Construct all default config handlers.
flags := newFlags()

// Declare flags.
cmd.PersistBuildImageCommandFlags(flags)
cmd.PersistConfigCommandFlags(flags)
cmd.PersistGlobalCommandFlags(flags)
cmd.PersistNanosVersionCommandFlags(flags)
cmd.PersistNightlyCommandFlags(flags)
cmd.PersistPkgCommandFlags(flags)
cmd.PersistProviderCommandFlags(flags)

// Set some platform specific settings.
platformCfg.SetFlags(flags)

// Initialize config containers.
configFlags := cmd.NewConfigCommandFlags(flags)
globalFlags := cmd.NewGlobalCommandFlags(flags)
nightlyFlags := cmd.NewNightlyCommandFlags(flags)
nanosVersionFlags := cmd.NewNanosVersionCommandFlags(flags)
buildImageFlags := cmd.NewBuildImageCommandFlags(flags)
providerFlags := cmd.NewProviderCommandFlags(flags)
pkgFlags := cmd.NewPkgCommandFlags(flags)

return &Factory{
defaults: cmd.NewMergeConfigContainer(configFlags, globalFlags, nightlyFlags, nanosVersionFlags, buildImageFlags, providerFlags, pkgFlags),
platformConfigurator: platformCfg,
}
}

func getPlatformConfig() (PlatformConfig, error) {
var platform PlatformConfig

switch viper.GetString("platform") {
case "aws":
platform = &AWSPlatformConfig{}
case "gcp":
platform = &GCPPlatformConfig{}
case "azure":
log.Fatal("Azure is not supported at the moment")
}

err := platform.load()
if err != nil {
return nil, err
}

return platform, nil
}

// TODO: program param is reserved for program specific configuration lookups.
func (f *Factory) NewConfig(program string) *types.Config {
cfg := api.NewConfig()
err := f.defaults.Merge(cfg)
if err != nil {
// There are no user passed parameters involved here so there should be no
// way for `Merge(cfg)` to return an error here.
log.Fatalf("failed to default config: %#v", err)
}

// Finalize last minute adjustments to config ;)
f.platformConfigurator.FinalizeConfig(cfg)

return cfg
}

func newFlags() *pflag.FlagSet {
// TODO: Figure out if flags.SetOutput(new(bytes.Buffer)) is needed.
return pflag.NewFlagSet("<placeholder>", pflag.ContinueOnError)
}
Loading

0 comments on commit 0fc9b7b

Please sign in to comment.