From 2ba0555a25ffa4bd03d0a4bfaf36344266f80709 Mon Sep 17 00:00:00 2001 From: Ric Featherstone Date: Wed, 1 Nov 2023 10:39:28 +0000 Subject: [PATCH] feat: manage infrastructure --- .dockerignore | 19 +++ .gitignore | 6 + Dockerfile | 7 + Makefile | 31 ++++ ansible/init-cluster.yaml | 61 +++++++ ansible/update-known-hosts.yaml | 8 + aws-environment | 5 + config/.gitkeep | 0 controlplane/aws/aws.go | 55 +++++++ controlplane/cli/image.go | 37 +++++ controlplane/cli/infra.go | 47 ++++++ controlplane/cli/scenario.go | 46 ++++++ controlplane/cli/simulator.go | 18 +++ controlplane/cli/storage.go | 33 ++++ controlplane/cmd/main.go | 35 ++++ controlplane/commands/ansible.go | 30 ++++ controlplane/commands/packer.go | 37 +++++ controlplane/commands/runner.go | 57 +++++++ controlplane/commands/terraform.go | 49 ++++++ controlplane/simulator.go | 133 +++++++++++++++ dev.Dockerfile | 44 +++++ docs/terraform-aws-sso.md | 31 ++++ go.mod | 54 ++++++- go.sum | 142 ++++++++++++++++ internal/cli/config.go | 45 ++++++ internal/cli/image.go | 47 ++++++ internal/cli/infra.go | 70 ++++++++ internal/cli/scenario.go | 128 +++++++++++++++ internal/cli/simulator.go | 36 +++++ internal/cli/storage.go | 41 +++++ internal/cmd/main.go | 9 ++ internal/config/config.go | 90 +++++++++++ internal/config/config.yaml | 5 + internal/container/runner.go | 153 ++++++++++++++++++ scenarios/scenarios.go | 66 ++++++++ terraform/modules/cluster/bastion.tf | 77 +++++++++ terraform/modules/cluster/cloud-config.tf | 21 +++ terraform/modules/cluster/instances.tf | 67 ++++++++ terraform/modules/cluster/key-pairs.tf | 33 ++++ terraform/modules/cluster/locals.tf | 32 ++++ .../modules/instance-group/instances.tf | 44 +++++ .../cluster/modules/instance-group/outputs.tf | 4 + .../modules/instance-group/terraform.tf | 12 ++ .../modules/instance-group/variables.tf | 63 ++++++++ terraform/modules/cluster/outputs.tf | 27 ++++ .../modules/cluster/templates/ansible.cfg | 6 + .../cluster/templates/cloud-config.yaml | 12 ++ .../modules/cluster/templates/inventory.yaml | 17 ++ .../modules/cluster/templates/ssh_config | 17 ++ terraform/modules/cluster/terraform.tf | 9 ++ terraform/modules/cluster/variables.tf | 71 ++++++++ terraform/modules/network/locals.tf | 4 + terraform/modules/network/network.tf | 53 ++++++ terraform/modules/network/outputs.tf | 15 ++ terraform/modules/network/private.tf | 33 ++++ terraform/modules/network/public.tf | 33 ++++ terraform/modules/network/terraform.tf | 9 ++ terraform/modules/network/variables.tf | 18 +++ .../workspaces/simulator/.terraform.lock.hcl | 138 ++++++++++++++++ .../workspaces/simulator/admin_config.tf | 80 +++++++++ terraform/workspaces/simulator/main.tf | 136 ++++++++++++++++ .../workspaces/simulator/player_config.tf | 46 ++++++ 62 files changed, 2751 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 ansible/init-cluster.yaml create mode 100644 ansible/update-known-hosts.yaml create mode 100644 aws-environment create mode 100644 config/.gitkeep create mode 100644 controlplane/aws/aws.go create mode 100644 controlplane/cli/image.go create mode 100644 controlplane/cli/infra.go create mode 100644 controlplane/cli/scenario.go create mode 100644 controlplane/cli/simulator.go create mode 100644 controlplane/cli/storage.go create mode 100644 controlplane/cmd/main.go create mode 100644 controlplane/commands/ansible.go create mode 100644 controlplane/commands/packer.go create mode 100644 controlplane/commands/runner.go create mode 100644 controlplane/commands/terraform.go create mode 100644 controlplane/simulator.go create mode 100644 dev.Dockerfile create mode 100644 docs/terraform-aws-sso.md create mode 100644 go.sum create mode 100644 internal/cli/config.go create mode 100644 internal/cli/image.go create mode 100644 internal/cli/infra.go create mode 100644 internal/cli/scenario.go create mode 100644 internal/cli/simulator.go create mode 100644 internal/cli/storage.go create mode 100644 internal/cmd/main.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config.yaml create mode 100644 internal/container/runner.go create mode 100644 scenarios/scenarios.go create mode 100644 terraform/modules/cluster/bastion.tf create mode 100644 terraform/modules/cluster/cloud-config.tf create mode 100644 terraform/modules/cluster/instances.tf create mode 100644 terraform/modules/cluster/key-pairs.tf create mode 100644 terraform/modules/cluster/locals.tf create mode 100644 terraform/modules/cluster/modules/instance-group/instances.tf create mode 100644 terraform/modules/cluster/modules/instance-group/outputs.tf create mode 100644 terraform/modules/cluster/modules/instance-group/terraform.tf create mode 100644 terraform/modules/cluster/modules/instance-group/variables.tf create mode 100644 terraform/modules/cluster/outputs.tf create mode 100644 terraform/modules/cluster/templates/ansible.cfg create mode 100644 terraform/modules/cluster/templates/cloud-config.yaml create mode 100644 terraform/modules/cluster/templates/inventory.yaml create mode 100644 terraform/modules/cluster/templates/ssh_config create mode 100644 terraform/modules/cluster/terraform.tf create mode 100644 terraform/modules/cluster/variables.tf create mode 100644 terraform/modules/network/locals.tf create mode 100644 terraform/modules/network/network.tf create mode 100644 terraform/modules/network/outputs.tf create mode 100644 terraform/modules/network/private.tf create mode 100644 terraform/modules/network/public.tf create mode 100644 terraform/modules/network/terraform.tf create mode 100644 terraform/modules/network/variables.tf create mode 100644 terraform/workspaces/simulator/.terraform.lock.hcl create mode 100644 terraform/workspaces/simulator/admin_config.tf create mode 100644 terraform/workspaces/simulator/main.tf create mode 100644 terraform/workspaces/simulator/player_config.tf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..1e1a78cc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.idea/ +.git/ +.simulator/ +bin/ +config/ +docs/ +internal/ +simulation-scripts/ +terraform/workspaces/simulator/.terraform/ +.dockerignore +.editorconfig +.gitignore +aws-environment +dev.Dockerfile +Dockerfile +LICENSE +Makefile +README.md +SECURITY.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..584fcefa --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/ +.simulator/ +config/* +!config/.gitkeep +bin/ +terraform/workspaces/simulator/.terraform/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..8249759f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM controlplane/simulator:dev + +COPY --chown=ubuntu:ubuntu packer packer +COPY --chown=ubuntu:ubuntu terraform terraform +COPY --chown=ubuntu:ubuntu scenarios scenarios + +RUN cd terraform/workspaces/simulator && terraform init -backend=false diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..71f6ceb4 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +SIMULATOR_IMAGE ?= controlplane/simulator + +simulator-dev-image: + docker build -t $(SIMULATOR_IMAGE):dev -f dev.Dockerfile . + +simulator-dev-image-run: + docker run -it --rm \ + -v ~/.aws:/home/ubuntu/.aws \ + -v $(shell pwd)/ansible:/simulator/ansible \ + -v $(shell pwd)/config:/simulator/config:rw \ + -v $(shell pwd)/packer:/simulator/packer \ + -v $(shell pwd)/terraform:/simulator/terraform \ + --env-file aws-environment \ + --entrypoint bash \ + $(SIMULATOR_IMAGE):dev \ + +simulator-image: simulator-dev-image + docker build -t $(SIMULATOR_IMAGE) . + +simulator-image-run: + docker run -it --rm \ + -v ~/.aws:/home/ubuntu/.aws \ + -v $(shell pwd)/config:/simulator/config:rw \ + --env-file aws-environment \ + --entrypoint bash \ + $(SIMULATOR_IMAGE):latest \ + +simulator-cli: + go build -v -o bin/simulator internal/cmd/main.go + + diff --git a/ansible/init-cluster.yaml b/ansible/init-cluster.yaml new file mode 100644 index 00000000..bb61a44a --- /dev/null +++ b/ansible/init-cluster.yaml @@ -0,0 +1,61 @@ +--- + +- name: Initialise the kubernetes cluster + hosts: all + gather_facts: no + become: yes + tasks: + - name: Run kubeadm init + ansible.builtin.shell: + cmd: kubeadm init + creates: /etc/kubernetes/admin.conf + when: inventory_hostname in groups['masters'][0] + + - name: Create join command + ansible.builtin.shell: kubeadm token create --print-join-command + become: yes + register: join_command + when: inventory_hostname in groups['masters'][0] + + - name: Join nodes + ansible.builtin.shell: "{{ hostvars[groups['masters'][0]].join_command.stdout }}" + when: inventory_hostname in groups['nodes'] + + - name: Create .kube directories on bastion + ansible.builtin.file: + path: "/home/{{ item }}/.kube" + state: directory + owner: "{{ item }}" + group: "{{ item }}" + mode: '0755' + loop: + - ubuntu + - player + when: "'bastion' in inventory_hostname" + + - name: Retrieve kubeconfig from master + ansible.builtin.fetch: + src: /etc/kubernetes/admin.conf + dest: kubeconfig + flat: yes + when: "'master-1' in inventory_hostname" + + - name: Copy kubeconfig to bastion + ansible.builtin.copy: + src: kubeconfig + dest: "/home/{{ item }}/.kube/config" + owner: "{{ item }}" + group: "{{ item }}" + mode: 0440 + loop: + - ubuntu + - player + when: "'bastion' in inventory_hostname" + + - name: Remove pulled kubeconfig + ansible.builtin.file: + path: kubeconfig + state: absent + become: no + run_once: yes + delegate_to: localhost diff --git a/ansible/update-known-hosts.yaml b/ansible/update-known-hosts.yaml new file mode 100644 index 00000000..571aff25 --- /dev/null +++ b/ansible/update-known-hosts.yaml @@ -0,0 +1,8 @@ +--- + +- hosts: all + gather_facts: no + become: no + tasks: + - ansible.builtin.wait_for_connection: + - ansible.builtin.ping: diff --git a/aws-environment b/aws-environment new file mode 100644 index 00000000..05f5f543 --- /dev/null +++ b/aws-environment @@ -0,0 +1,5 @@ +AWS_PROFILE +AWS_REGION +AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY +AWS_SESSION_TOKEN diff --git a/config/.gitkeep b/config/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/controlplane/aws/aws.go b/controlplane/aws/aws.go new file mode 100644 index 00000000..3322e76d --- /dev/null +++ b/controlplane/aws/aws.go @@ -0,0 +1,55 @@ +package aws + +import ( + "context" + "fmt" + "log/slog" + "os" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3/types" +) + +var ( + Env []string +) + +func CreateBucket(ctx context.Context, name string) error { + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + slog.Error("failed to create aws config", "error", err) + } + + client := s3.NewFromConfig(cfg) + _, err = client.CreateBucket(ctx, &s3.CreateBucketInput{ + Bucket: aws.String(name), + CreateBucketConfiguration: &types.CreateBucketConfiguration{ + LocationConstraint: types.BucketLocationConstraintEuWest2, // TODO: lookup AWS_REGION + }, + }) + if err != nil { // TODO: ignore bucket already exists, and you own it + slog.Error("failed to create s3 bucket", "error", err) + return err + } + + return nil +} + +func init() { + envKeys := []string{ + "AWS_PROFILE", + "AWS_REGION", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + } + + for _, key := range envKeys { + value, ok := os.LookupEnv(key) + if ok && len(value) > 0 { + Env = append(Env, fmt.Sprintf("%s=%s", key, value)) + } + } +} diff --git a/controlplane/cli/image.go b/controlplane/cli/image.go new file mode 100644 index 00000000..f41361f9 --- /dev/null +++ b/controlplane/cli/image.go @@ -0,0 +1,37 @@ +package cli + +import ( + "context" + "os" + "os/signal" + + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/controlplane" +) + +var ( + template string +) + +var imageCmd = &cobra.Command{ + Use: "image", +} + +var buildCmd = &cobra.Command{ + Use: "build", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + cp := controlplane.New() + return cp.BuildImage(ctx, controlplane.PackerTemplate(template)) + }, +} + +func init() { + buildCmd.Flags().StringVar(&template, "template", "", "the packer template to build") + + imageCmd.AddCommand(buildCmd) + simulatorCmd.AddCommand(imageCmd) +} diff --git a/controlplane/cli/infra.go b/controlplane/cli/infra.go new file mode 100644 index 00000000..73b51299 --- /dev/null +++ b/controlplane/cli/infra.go @@ -0,0 +1,47 @@ +package cli + +import ( + "context" + "os" + "os/signal" + + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/controlplane" +) + +var infraCmd = &cobra.Command{ + Use: "infra", +} + +var createCmd = &cobra.Command{ + Use: "create", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + cp := controlplane.New() + return cp.CreateInfrastructure(ctx, bucket, key, name) + }, +} + +var destroyCmd = &cobra.Command{ + Use: "destroy", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + cp := controlplane.New() + return cp.DestroyInfrastructure(ctx, bucket, key, name) + }, +} + +func init() { + infraCmd.PersistentFlags().StringVar(&bucket, "bucket", "", "the s3 bucket to use") + infraCmd.PersistentFlags().StringVar(&key, "key", "state/terraform.tfstate", "the key to store state in the s3 bucket") + infraCmd.PersistentFlags().StringVar(&name, "name", "", "the name for the infrastructure") + + infraCmd.AddCommand(createCmd) + infraCmd.AddCommand(destroyCmd) + simulatorCmd.AddCommand(infraCmd) +} diff --git a/controlplane/cli/scenario.go b/controlplane/cli/scenario.go new file mode 100644 index 00000000..2c8eeeed --- /dev/null +++ b/controlplane/cli/scenario.go @@ -0,0 +1,46 @@ +package cli + +import ( + "context" + "os" + "os/signal" + + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/controlplane" +) + +var scenarioCmd = &cobra.Command{ + Use: "scenario", +} + +var installCmd = &cobra.Command{ + Use: "install", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + cp := controlplane.New() + return cp.InstallScenario(ctx, name) + }, +} + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + cp := controlplane.New() + return cp.UninstallScenario(ctx, name) + }, +} + +func init() { + scenarioCmd.PersistentFlags().StringVar(&name, "name", "", "the name of the scenario to deploy") + + scenarioCmd.AddCommand(installCmd) + scenarioCmd.AddCommand(uninstallCmd) + simulatorCmd.AddCommand(scenarioCmd) + +} diff --git a/controlplane/cli/simulator.go b/controlplane/cli/simulator.go new file mode 100644 index 00000000..6a3657de --- /dev/null +++ b/controlplane/cli/simulator.go @@ -0,0 +1,18 @@ +package cli + +import ( + "github.com/spf13/cobra" +) + +var ( + bucket, key, name string +) + +var simulatorCmd = &cobra.Command{ + Use: "simulator", +} + +func Execute() { + err := simulatorCmd.Execute() + cobra.CheckErr(err) +} diff --git a/controlplane/cli/storage.go b/controlplane/cli/storage.go new file mode 100644 index 00000000..33a9e662 --- /dev/null +++ b/controlplane/cli/storage.go @@ -0,0 +1,33 @@ +package cli + +import ( + "context" + "os" + "os/signal" + + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/controlplane" +) + +var bucketCmd = &cobra.Command{ + Use: "bucket", +} + +var createBucketCmd = &cobra.Command{ + Use: "create", + RunE: func(cmd *cobra.Command, args []string) error { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + cp := controlplane.New() + return cp.CreateBucket(ctx, bucket) + }, +} + +func init() { + createBucketCmd.Flags().StringVar(&bucket, "name", "", "the name of the bucket to create") + + bucketCmd.AddCommand(createBucketCmd) + simulatorCmd.AddCommand(bucketCmd) +} diff --git a/controlplane/cmd/main.go b/controlplane/cmd/main.go new file mode 100644 index 00000000..dd088d8d --- /dev/null +++ b/controlplane/cmd/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "log/slog" + "os" + + "github.com/controlplaneio/simulator/controlplane/cli" +) + +const ( + LogLevel = "LOG_LEVEL" +) + +func main() { + level, ok := os.LookupEnv(LogLevel) + if !ok { + level = "info" + } + + var sLevel slog.Level + + switch level { + case "debug": + sLevel = slog.LevelDebug + case "info": + sLevel = slog.LevelInfo + } + + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: sLevel, + })) + slog.SetDefault(logger) + + cli.Execute() +} diff --git a/controlplane/commands/ansible.go b/controlplane/commands/ansible.go new file mode 100644 index 00000000..e42c0a4d --- /dev/null +++ b/controlplane/commands/ansible.go @@ -0,0 +1,30 @@ +package commands + +import ( + "fmt" + "strings" +) + +const ( + AnsiblePlaybook Executable = "ansible-playbook" +) + +func AnsiblePlaybookCommand(workingDir, playbook string, extraVars ...string) Runnable { + args := []string{ + fmt.Sprintf("%s/%s.yaml", workingDir, playbook), + } + + if len(extraVars) > 0 { + args = append(args, + "--extra-vars", + fmt.Sprintf("\"%s\"", strings.Join(extraVars, " ")), + ) + } + + return command{ + Executable: AnsiblePlaybook, + WorkingDir: workingDir, + Environment: nil, + Arguments: args, + } +} diff --git a/controlplane/commands/packer.go b/controlplane/commands/packer.go new file mode 100644 index 00000000..dbfcfeef --- /dev/null +++ b/controlplane/commands/packer.go @@ -0,0 +1,37 @@ +package commands + +import ( + "github.com/controlplaneio/simulator/controlplane/aws" +) + +const ( + Packer Executable = "packer" +) + +func PackerInitCommand(workingDir, template string) Runnable { + args := []string{ + "init", + template, + } + + return command{ + Executable: Packer, + WorkingDir: workingDir, + Environment: aws.Env, + Arguments: args, + } +} + +func PackerBuildCommand(workingDir, template string) Runnable { + args := []string{ + "build", + template, + } + + return command{ + Executable: Packer, + WorkingDir: workingDir, + Environment: aws.Env, + Arguments: args, + } +} diff --git a/controlplane/commands/runner.go b/controlplane/commands/runner.go new file mode 100644 index 00000000..405a630b --- /dev/null +++ b/controlplane/commands/runner.go @@ -0,0 +1,57 @@ +package commands + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/exec" + "strings" +) + +type Executable string + +type Runnable interface { + Run(ctx context.Context) error +} + +type command struct { + Executable Executable + WorkingDir string + Environment []string + Arguments []string +} + +func (c command) Run(ctx context.Context) error { + cmd := exec.CommandContext(ctx, string(c.Executable), c.Arguments...) + cmd.Dir = c.WorkingDir + cmd.Env = c.Environment + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + slog.Debug("running", "command", c) + + // TODO: Ensure ctrl-c stops the command + err := cmd.Run() + if err != nil { + slog.Error("failed to run", "command", c) + } + + return err +} + +func (c command) LogValue() slog.Value { + cmd := fmt.Sprintf("%s %s", c.Executable, strings.Join(c.Arguments, " ")) + var env []string + + // Only log env keys, not values + for _, value := range c.Environment { + env = append(env, value[:strings.Index(value, "=")]) + } + + return slog.GroupValue( + slog.String("cmd", cmd), + slog.String("dir", c.WorkingDir), + slog.String("env", strings.Join(env, ",")), + ) +} diff --git a/controlplane/commands/terraform.go b/controlplane/commands/terraform.go new file mode 100644 index 00000000..e364b0a4 --- /dev/null +++ b/controlplane/commands/terraform.go @@ -0,0 +1,49 @@ +package commands + +import ( + "github.com/controlplaneio/simulator/controlplane/aws" +) + +type TerraformCommandType bool + +const ( + Terraform Executable = "terraform" + TerraformApply TerraformCommandType = true + TerraformDestroy TerraformCommandType = false +) + +func TerraformInitCommand(workingDir string, backendConfig []string) Runnable { + args := []string{ + "init", + "-reconfigure", + } + + args = append(args, backendConfig...) + + return command{ + Executable: Terraform, + WorkingDir: workingDir, + Environment: aws.Env, + Arguments: args, + } +} + +func TerraformCommand(workingDir string, apply TerraformCommandType, vars []string) Runnable { + args := []string{ + "apply", + "-auto-approve", + } + + args = append(args, vars...) + + if !apply { + args = append(args, "-destroy") + } + + return command{ + Executable: Terraform, + WorkingDir: workingDir, + Environment: aws.Env, + Arguments: args, + } +} diff --git a/controlplane/simulator.go b/controlplane/simulator.go new file mode 100644 index 00000000..726eefbd --- /dev/null +++ b/controlplane/simulator.go @@ -0,0 +1,133 @@ +package controlplane + +import ( + "context" + "fmt" + "log/slog" + "path/filepath" + + "github.com/controlplaneio/simulator/controlplane/aws" + "github.com/controlplaneio/simulator/controlplane/commands" +) + +type PackerTemplate string + +const ( + BastionImage PackerTemplate = "bastion" + K8sImage PackerTemplate = "k8s" + + SimulatorDir = "/simulator" + Home = "config" + Scenarios = "scenarios" + Packer = "packer" + Terraform = "terraform" +) + +var ( + HomeDir = filepath.Join(SimulatorDir, Home) + AdminConfigDir = filepath.Join(HomeDir, "admin") + PlayerConfigDir = filepath.Join(HomeDir, "player") + + AnsibleDir = filepath.Join(SimulatorDir, Scenarios) + AnsiblePlaybookDir string = filepath.Join(AnsibleDir, "playbooks") + + PackerTemplateDir string = filepath.Join(SimulatorDir, Packer) + + TerraformDir = filepath.Join(SimulatorDir, Terraform) + TerraformWorkspaceDir = filepath.Join(TerraformDir, "workspaces/simulator") +) + +func New() Simulator { + return simulator{} +} + +type Simulator interface { + // CreateBucket creates the S3 bucket used to store Simulator state. + CreateBucket(ctx context.Context, name string) error + // BuildImage runs Packer to create the specified AMI. + BuildImage(ctx context.Context, name PackerTemplate) error + // CreateInfrastructure runs Terraform to create the Simulator infrastructure. + CreateInfrastructure(ctx context.Context, bucket string, key string, name string) error + // DestroyInfrastructure runs Terraform to destroy the Simulator infrastructure. + DestroyInfrastructure(ctx context.Context, bucket string, key string, name string) error + // InstallScenario installs a scenario on the Simulator infrastructure. + InstallScenario(ctx context.Context, name string) error + // UninstallScenario uninstalls a scenario from the Simulator infrastructure. + UninstallScenario(ctx context.Context, name string) error +} + +type simulator struct { +} + +func (s simulator) CreateBucket(ctx context.Context, name string) error { + slog.Debug("simulator init", "name", name) + + return aws.CreateBucket(ctx, name) +} + +func (s simulator) BuildImage(ctx context.Context, name PackerTemplate) error { + slog.Debug("simulator build", "image", name) + + err := commands.PackerInitCommand(PackerTemplateDir, string(name)).Run(ctx) + if err != nil { + return err + } + + return commands.PackerBuildCommand(PackerTemplateDir, string(name)).Run(ctx) +} + +func (s simulator) CreateInfrastructure(ctx context.Context, bucket, key, name string) error { + slog.Debug("simulator create infrastructure", "bucket", bucket, "key", key, "name", name) + + err := commands.TerraformInitCommand(TerraformWorkspaceDir, backendConfig(bucket, key)).Run(ctx) + if err != nil { + return err + } + + return commands.TerraformCommand(TerraformWorkspaceDir, commands.TerraformApply, terraformVars(name, bucket)).Run(ctx) +} + +func (s simulator) DestroyInfrastructure(ctx context.Context, bucket, key, name string) error { + slog.Debug("simulator destroy infrastructure", "bucket", bucket, "key", key, "name", name) + + err := commands.TerraformInitCommand(TerraformWorkspaceDir, backendConfig(bucket, key)).Run(ctx) + if err != nil { + return err + } + + return commands.TerraformCommand(TerraformWorkspaceDir, commands.TerraformDestroy, terraformVars(name, bucket)).Run(ctx) +} + +func (s simulator) InstallScenario(ctx context.Context, name string) error { + slog.Debug("simulator install", "scenario", name) + + return commands.AnsiblePlaybookCommand(AdminConfigDir, name).Run(ctx) +} + +func (s simulator) UninstallScenario(ctx context.Context, name string) error { + slog.Debug("simulator uninstall", "scenario", name) + + return commands.AnsiblePlaybookCommand(name, "state=absent").Run(ctx) +} + +func backendConfig(bucket, key string) []string { + return []string{ + "-backend-config", + fmt.Sprintf("bucket=%s", bucket), + "-backend-config", + fmt.Sprintf("key=%s", key), + } +} + +func terraformVars(name, bucket string) []string { + return []string{ + "-var", + fmt.Sprintf("name=%s", name), + "-var", + fmt.Sprintf("bucket=%s", bucket), + "-var", + fmt.Sprintf("admin_config_dir=%s", AdminConfigDir), + "-var", + fmt.Sprintf("player_config_dir=%s", PlayerConfigDir), + } +} diff --git a/dev.Dockerfile b/dev.Dockerfile new file mode 100644 index 00000000..f1a1fda1 --- /dev/null +++ b/dev.Dockerfile @@ -0,0 +1,44 @@ +ARG GOLANG_IMAGE=golang:1.21.3-alpine3.18@sha256:27c76dcf886c5024320f4fa8ceb57d907494a3bb3d477d0aa7ac8385acd871ea +ARG GOLANGCI_LINT_IMAGE=golangci/golangci-lint:latest@sha256:c87d8a1a6521748fee124920c8e9302934ed26c9d3d48982449192b420a34686 +ARG PACKER_IMAGE=hashicorp/packer:1.9@sha256:03808122fbfdd88e03be0d21cce9b3317778319b415c77e88efe1a98db82c76a +ARG TERRAFORM_IMAGE=hashicorp/terraform:1.5@sha256:c3bc74e7a2a8fab8216cbbedf12a9637db09288806a6aa537b6f397cba04dd93 +ARG UBUNTU_IMAGE=ubuntu:mantic@sha256:13f233a16be210b57907b98b0d927ceff7571df390701e14fe1f3901b2c4a4d7 + +FROM ${GOLANGCI_LINT_IMAGE} + +WORKDIR /app + +COPY . . + +RUN /usr/bin/golangci-lint run -v + +FROM ${GOLANG_IMAGE} as BUILDER + +WORKDIR /build + +COPY go.* ./ +RUN go mod download + +COPY . . + +RUN go build -v -o /simulator controlplane/cmd/main.go + +FROM ${PACKER_IMAGE} as PACKER +FROM ${TERRAFORM_IMAGE} as TERRAFORM +FROM ${UBUNTU_IMAGE} + +WORKDIR simulator + +COPY --from=PACKER /bin/packer /usr/local/bin/packer +COPY --from=TERRAFORM /bin/terraform /usr/local/bin/terraform + +RUN apt update && \ + apt install -y ca-certificates openssh-client ansible-core && \ + rm -rf /var/lib/apt/lists/* && \ + ansible-galaxy collection install kubernetes.core + +COPY --from=BUILDER /simulator /usr/local/bin/simulator + +USER ubuntu + +ENTRYPOINT ["/usr/local/bin/simulator"] diff --git a/docs/terraform-aws-sso.md b/docs/terraform-aws-sso.md new file mode 100644 index 00000000..9d1547e5 --- /dev/null +++ b/docs/terraform-aws-sso.md @@ -0,0 +1,31 @@ +# Terraform AWS SSO Issue + +Terraform doesn't support SSO correctly until v1.6. + +You should be able to create a specific profile for running Terraform as detailed +[here](https://github.com/gruntwork-io/terragrunt/issues/2604#issuecomment-1692391611). + +If that doesn't work, and you get an error like this: + +```shell +Initializing the backend... +╷ +│ Error: error configuring S3 Backend: no valid credential sources for S3 Backend found. +│ +│ Please see https://www.terraform.io/docs/language/settings/backends/s3.html +│ for more information about providing credentials. +│ +│ Error: ProcessProviderExecutionError: error in credential_process +│ caused by: exec: "sh": executable file not found in $PATH +``` + +You can export the required environment variables like this: + +```shell +export AWS_REGION=... +aws sso login --profile simulator +source <(aws configure export-credentials --profile simulator --format env) +``` + +Be careful with timeouts, as this does not last as long as regular SSO credentials, so source the environment variables +before you run a Terraform or Packer command diff --git a/go.mod b/go.mod index d3a333af..c1b6ee20 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,55 @@ module github.com/controlplaneio/simulator -go 1.21.0 +go 1.21 + +require ( + github.com/aws/aws-sdk-go-v2 v1.21.2 + github.com/aws/aws-sdk-go-v2/config v1.19.0 + github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2 + github.com/docker/docker v24.0.6+incompatible + github.com/opencontainers/image-spec v1.0.2 + github.com/spf13/cobra v1.7.0 + gopkg.in/yaml.v2 v2.2.8 +) + +require ( + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // indirect + github.com/aws/smithy-go v1.15.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.8.4 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.13.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gotest.tools/v3 v3.5.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..2f520ed9 --- /dev/null +++ b/go.sum @@ -0,0 +1,142 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= +github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14 h1:Sc82v7tDQ/vdU1WtuSyzZ1I7y/68j//HJ6uozND1IDs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.14/go.mod h1:9NCTOURS8OpxvoAVHq79LK81/zC78hfRWFn+aL0SPcY= +github.com/aws/aws-sdk-go-v2/config v1.19.0 h1:AdzDvwH6dWuVARCl3RTLGRc4Ogy+N7yLFxVxXe1ClQ0= +github.com/aws/aws-sdk-go-v2/config v1.19.0/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6 h1:wmGLw2i8ZTlHLw7a9ULGfQbuccw8uIiNr6sol5bFzc8= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.6/go.mod h1:Q0Hq2X/NuL7z8b1Dww8rmOFl+jzusKEcyvkKspwdpyc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15 h1:7R8uRYyXzdD71KWVCL78lJZltah6VVznXBazvKjfH58= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.15/go.mod h1:26SQUPcTNgV1Tapwdt4a1rOsYRsnBsJHLMPoxK2b0d8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38 h1:skaFGzv+3kA+v2BPKhuekeb1Hbb105+44r8ASC+q5SE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.38/go.mod h1:epIZoRSSbRIwLPJU5F+OldHhwZPBdpDeQkRdCeY3+00= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6 h1:9ulSU5ClouoPIYhDQdg9tpl83d5Yb91PXTKK+17q+ow= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.6/go.mod h1:lnc2taBsR9nTlz9meD+lhFZZ9EWY712QHrRflWpTcOA= +github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2 h1:Ll5/YVCOzRB+gxPqs2uD0R7/MyATC0w85626glSKmp4= +github.com/aws/aws-sdk-go-v2/service/s3 v1.40.2/go.mod h1:Zjfqt7KhQK+PO1bbOsFNzKgaq7TcxzmEoDWN8lM0qzQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= +github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= +github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8= +github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= +github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/internal/cli/config.go b/internal/cli/config.go new file mode 100644 index 00000000..41fe3bb6 --- /dev/null +++ b/internal/cli/config.go @@ -0,0 +1,45 @@ +package cli + +import ( + "os" + + "github.com/spf13/cobra" +) + +var name, bucket string +var dev bool + +var configCmd = &cobra.Command{ + Use: "config", + Short: "Configure the simulator cli", + Run: func(cmd *cobra.Command, args []string) { + baseDir, err := os.Getwd() + cobra.CheckErr(err) + + cfg.BaseDir = baseDir + + if name != "" { + cfg.Name = name + } + + if bucket != "" { + cfg.Bucket = bucket + } + + if dev { + cfg.Cli.Dev = true + cfg.Container.Image = "controlplane/simulator:dev" + } else { + cfg.Cli.Dev = false + cfg.Container.Image = "controlplane/simulator:latest" + } + }, +} + +func init() { + configCmd.PersistentFlags().StringVar(&name, "name", "simulator", "the name for the infrastructure") + configCmd.PersistentFlags().StringVar(&bucket, "bucket", "", "the s3 bucket used for storage") + configCmd.PersistentFlags().BoolVar(&dev, "dev", false, "developer mode") + + simulatorCmd.AddCommand(configCmd) +} diff --git a/internal/cli/image.go b/internal/cli/image.go new file mode 100644 index 00000000..e3d83656 --- /dev/null +++ b/internal/cli/image.go @@ -0,0 +1,47 @@ +package cli + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/internal/container" +) + +var imageCmd = &cobra.Command{ + Use: "image", +} + +var template string + +var imageBuildCmd = &cobra.Command{ + Use: "build", + Short: "Build the packer image", + Run: func(cmd *cobra.Command, args []string) { + runner := container.New(cfg) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + command := []string{ + "image", + "build", + "--template", + fmt.Sprintf("%s.pkr.hcl", template), + } + + err := runner.Run(ctx, command) + cobra.CheckErr(err) + }, +} + +func init() { + imageBuildCmd.Flags().StringVar(&template, "template", "", "the packer template to build; bastion, or k8s") + imageCmd.AddCommand(imageBuildCmd) + + simulatorCmd.AddCommand(imageCmd) +} diff --git a/internal/cli/infra.go b/internal/cli/infra.go new file mode 100644 index 00000000..ff5a6cef --- /dev/null +++ b/internal/cli/infra.go @@ -0,0 +1,70 @@ +package cli + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/internal/container" +) + +var infraCmd = &cobra.Command{ + Use: "infra [command]", + Short: "Manage the simulator infrastructure", +} + +var infraCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create simulator infrastructure", + Run: func(cmd *cobra.Command, args []string) { + runner := container.New(cfg) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + command := []string{ + "infra", + "create", + "--bucket", + cfg.Bucket, + "--name", + cfg.Name, + } + + err := runner.Run(ctx, command) + cobra.CheckErr(err) + }, +} + +var infraDestroyCmd = &cobra.Command{ + Use: "destroy", + Short: "Destroy simulator infrastructure", + Run: func(cmd *cobra.Command, args []string) { + runner := container.New(cfg) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + command := []string{ + "infra", + "destroy", + "--bucket", + cfg.Bucket, + "--name", + cfg.Name, + } + + err := runner.Run(ctx, command) + cobra.CheckErr(err) + }, +} + +func init() { + infraCmd.AddCommand(infraCreateCmd) + infraCmd.AddCommand(infraDestroyCmd) + + simulatorCmd.AddCommand(infraCmd) +} diff --git a/internal/cli/scenario.go b/internal/cli/scenario.go new file mode 100644 index 00000000..4356eef3 --- /dev/null +++ b/internal/cli/scenario.go @@ -0,0 +1,128 @@ +package cli + +import ( + "context" + "fmt" + "os" + "os/signal" + + "github.com/olekukonko/tablewriter" + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/internal/container" + "github.com/controlplaneio/simulator/scenarios" +) + +var scenarioCmd = &cobra.Command{ + Use: "scenario", + Short: "Manage the simulator scenario", +} + +var scenarioInstallCmd = &cobra.Command{ + Use: "install [id]", + Short: "Install the scenario into the simulator infrastructure", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + scenarioID := args[0] + + command := []string{ + "scenario", + "install", + "--name", + scenarioID, + } + + runner := container.New(cfg) + err := runner.Run(ctx, command) + cobra.CheckErr(err) + }, +} + +var scenarioUninstallCmd = &cobra.Command{ + Use: "uninstall [id]", + Short: "Uninstall the scenario into the simulator infrastructure", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + + scenarioID := args[0] + + command := []string{ + "scenario", + "uninstall", + "--name", + scenarioID, + } + + runner := container.New(cfg) + err := runner.Run(ctx, command) + cobra.CheckErr(err) + }, +} + +var scenarioListCmd = &cobra.Command{ + Use: "list", + Short: "List available scenarios", + Run: func(cmd *cobra.Command, args []string) { + scenarios, err := scenarios.List() + cobra.CheckErr(err) + + table := tablewriter.NewWriter(os.Stdout) + + table.SetHeader([]string{ + "ID", + "Name", + "Description", + "Category", + "Difficulty", + }) + + table.SetHeaderColor( + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.Bold}, + ) + + for _, s := range scenarios { + table.Append([]string{ + s.ID, + s.Name, + s.Description, + s.Category, + s.Difficulty}) + table.SetRowLine(true) + } + table.Render() + }, +} + +var scenarioDescribeCmd = &cobra.Command{ + Use: "describe [id]", + Short: "Describes a scenario", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + scenarioID := args[0] + + s, err := scenarios.Find(scenarioID) + cobra.CheckErr(err) + + b, err := s.Challenge() + cobra.CheckErr(err) + + fmt.Println(string(b)) + }, +} + +func init() { + scenarioCmd.AddCommand(scenarioInstallCmd) + scenarioCmd.AddCommand(scenarioUninstallCmd) + scenarioCmd.AddCommand(scenarioListCmd) + scenarioCmd.AddCommand(scenarioDescribeCmd) + simulatorCmd.AddCommand(scenarioCmd) +} diff --git a/internal/cli/simulator.go b/internal/cli/simulator.go new file mode 100644 index 00000000..a2ba686d --- /dev/null +++ b/internal/cli/simulator.go @@ -0,0 +1,36 @@ +package cli + +import ( + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/internal/config" +) + +var cfg *config.Config + +var simulatorCmd = &cobra.Command{ + Use: "simulator", + Short: "Simulator CLI", +} + +func Execute() { + err := simulatorCmd.Execute() + cobra.CheckErr(err) +} + +func init() { + cfg = &config.Config{} + + cobra.OnInitialize(readConfig) + cobra.OnFinalize(writeConfig) +} + +func readConfig() { + err := cfg.Read + cobra.CheckErr(err()) +} + +func writeConfig() { + err := cfg.Write + cobra.CheckErr(err()) +} diff --git a/internal/cli/storage.go b/internal/cli/storage.go new file mode 100644 index 00000000..10c6e1c0 --- /dev/null +++ b/internal/cli/storage.go @@ -0,0 +1,41 @@ +package cli + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/spf13/cobra" + + "github.com/controlplaneio/simulator/internal/container" +) + +var bucketCmd = &cobra.Command{ + Use: "bucket", +} + +var createBucketCmd = &cobra.Command{ + Use: "create", + Run: func(cmd *cobra.Command, args []string) { + runner := container.New(cfg) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + command := []string{ + "bucket", + "create", + "--name", + cfg.Bucket, + } + + err := runner.Run(ctx, command) + cobra.CheckErr(err) + }, +} + +func init() { + bucketCmd.AddCommand(createBucketCmd) + simulatorCmd.AddCommand(bucketCmd) +} diff --git a/internal/cmd/main.go b/internal/cmd/main.go new file mode 100644 index 00000000..73c71fa3 --- /dev/null +++ b/internal/cmd/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/controlplaneio/simulator/internal/cli" +) + +func main() { + cli.Execute() +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 00000000..fcec4e90 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,90 @@ +package config + +import ( + "embed" + "errors" + "os" + "path/filepath" + + "gopkg.in/yaml.v2" +) + +const ( + Dir = "SIMULATOR_DIR" + FileName = "config.yaml" +) + +//go:embed config.yaml +var defaultConfig embed.FS + +type Config struct { + Name string `yaml:"name"` + Bucket string `yaml:"bucket"` + BaseDir string `yaml:"baseDir"` + + Cli struct { + Dev bool `yaml:"dev,omitempty"` + } `yaml:"cli,omitempty"` + + Container struct { + Image string `yaml:"image"` + } `yaml:"container"` +} + +func (c *Config) Read() error { + file := file() + + if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) { + dir := filepath.Dir(file) + if _, err := os.Stat(dir); err != nil { + err = os.MkdirAll(dir, 0755) + if err != nil { + return err + } + } + + config, err := defaultConfig.ReadFile(FileName) + if err != nil { + return err + } + + err = os.WriteFile(file, config, 0644) + if err != nil { + return err + } + } + + config, err := os.ReadFile(file) + if err != nil { + return err + } + + err = yaml.Unmarshal(config, &c) + if err != nil { + return err + } + + return nil +} + +func (c *Config) Write() error { + config, err := yaml.Marshal(&c) + if err != nil { + return err + } + + return os.WriteFile(file(), config, 0644) +} + +func file() string { + dir, ok := os.LookupEnv(Dir) + if !ok { + home, err := os.UserHomeDir() + if err != nil { + panic(err) + } + dir = filepath.Join(home, ".simulator") + } + + return filepath.Join(dir, FileName) +} diff --git a/internal/config/config.yaml b/internal/config/config.yaml new file mode 100644 index 00000000..c9d62b57 --- /dev/null +++ b/internal/config/config.yaml @@ -0,0 +1,5 @@ +name: simulator +cli: + dev: false +container: + image: controlplane/simulator:latest diff --git a/internal/container/runner.go b/internal/container/runner.go new file mode 100644 index 00000000..6f2a9bed --- /dev/null +++ b/internal/container/runner.go @@ -0,0 +1,153 @@ +package container + +import ( + "context" + "errors" + "io" + "log/slog" + "os" + "path/filepath" + "sync" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + v1 "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/controlplaneio/simulator/controlplane" + "github.com/controlplaneio/simulator/controlplane/aws" + "github.com/controlplaneio/simulator/internal/config" +) + +var ( + NoHome = errors.New("unable to determine your home directory") + NoClient = errors.New("unable to create docker client") + CreateFailed = errors.New("unable to create simulator container") + StartFailed = errors.New("unable to start simulator container") + AttachFailed = errors.New("unable to attach to simulator container") + + containerAwsDir = "/home/ubuntu/.aws" +) + +type Simulator interface { + Run(ctx context.Context, command []string) error +} + +func New(config *config.Config) Simulator { + return &simulator{ + Config: config, + } +} + +type simulator struct { + Config *config.Config +} + +func (r simulator) Run(ctx context.Context, command []string) error { + home, err := os.UserHomeDir() + if err != nil { + return NoHome + } + + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return NoClient + } + + mounts := []mount.Mount{ + { + Type: mount.TypeBind, + Source: filepath.Join(r.Config.BaseDir, controlplane.Home), + Target: controlplane.HomeDir, + ReadOnly: false, + }, + { + Type: mount.TypeBind, + Source: filepath.Join(home, ".aws"), + Target: containerAwsDir, + }, + } + + if r.Config.Cli.Dev { + mounts = append(mounts, []mount.Mount{ + { + Type: mount.TypeBind, + Source: filepath.Join(r.Config.BaseDir, controlplane.Scenarios), + Target: controlplane.AnsibleDir, + }, + { + Type: mount.TypeBind, + Source: filepath.Join(r.Config.BaseDir, controlplane.Packer), + Target: controlplane.PackerTemplateDir, + }, + { + Type: mount.TypeBind, + Source: filepath.Join(r.Config.BaseDir, controlplane.Terraform), + Target: controlplane.TerraformDir, + ReadOnly: false, + }, + }...) + } + + cont, err := cli.ContainerCreate(ctx, + &container.Config{ + Image: r.Config.Container.Image, + Env: aws.Env, + Cmd: command, + Tty: true, + AttachStdout: true, + AttachStderr: true, + }, + &container.HostConfig{ + Mounts: mounts, + }, + &network.NetworkingConfig{}, + &v1.Platform{}, + "", + ) + if err != nil { + return CreateFailed + } + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + err = cli.ContainerStop(ctx, cont.ID, container.StopOptions{}) + if err != nil { + slog.Warn("failed to stop container", "id", cont.ID, "err", err) + } + + err = cli.ContainerRemove(ctx, cont.ID, types.ContainerRemoveOptions{}) + if err != nil { + slog.Warn("failed to remove container", "id", cont.ID, "err", err) + } + }() + + hijack, err := cli.ContainerAttach(ctx, cont.ID, types.ContainerAttachOptions{ + Stream: true, + Stdout: true, + Stderr: true, + }) + if err != nil { + return AttachFailed + } + + err = cli.ContainerStart(ctx, cont.ID, types.ContainerStartOptions{}) + if err != nil { + return StartFailed + } + + var wg sync.WaitGroup + wg.Add(1) + go func() { + _, _ = io.Copy(os.Stdout, hijack.Reader) + defer wg.Done() + }() + + wg.Wait() + + return nil +} diff --git a/scenarios/scenarios.go b/scenarios/scenarios.go new file mode 100644 index 00000000..229246f6 --- /dev/null +++ b/scenarios/scenarios.go @@ -0,0 +1,66 @@ +package scenarios + +import ( + "embed" + "errors" + "fmt" + "log/slog" + + "gopkg.in/yaml.v2" + + "github.com/controlplaneio/simulator/controlplane" +) + +//go:embed scenarios.yaml roles/*/files/challenge.txt +var config embed.FS + +func List() ([]Scenario, error) { + var scenarios []Scenario + + b, err := config.ReadFile("scenarios.yaml") + if err != nil { + slog.Error("failed to load scenarios file") + return nil, err + } + + err = yaml.Unmarshal(b, &scenarios) + if err != nil { + slog.Error("failed to unmarshall scenarios") + return nil, err + } + + return scenarios, nil +} + +func Find(id string) (Scenario, error) { + var s Scenario + + scenarios, err := List() + if err != nil { + return s, err + } + + for _, scenario := range scenarios { + if scenario.ID == id { + return scenario, nil + } + } + + return s, errors.New("unable to find scenario") +} + +type Scenario struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Description string `yaml:"description"` + Category string `yaml:"category"` + Difficulty string `yaml:"difficulty"` +} + +func (s Scenario) Challenge() ([]byte, error) { + return config.ReadFile(fmt.Sprintf("roles/%s/files/challenge.txt", s.ID)) +} + +func (s Scenario) Playbook() string { + return fmt.Sprintf("%s/%s", controlplane.AnsiblePlaybookDir, s.ID) +} diff --git a/terraform/modules/cluster/bastion.tf b/terraform/modules/cluster/bastion.tf new file mode 100644 index 00000000..9e613c71 --- /dev/null +++ b/terraform/modules/cluster/bastion.tf @@ -0,0 +1,77 @@ +resource "aws_instance" "bastion" { + ami = var.bastion_ami_id + instance_type = var.bastion_instance_type + key_name = aws_key_pair.admin.id + subnet_id = var.public_subnet_id + associate_public_ip_address = true + vpc_security_group_ids = [ + aws_security_group.bastion.id, + ] + + # metadata_options { + # http_endpoint = "disabled" + # } + + user_data = < group + } + + name = format("%s %s", title(var.name), each.value.name) + group = each.value.name + instance_count = each.value.count + ami_id = each.value.ami_id + instance_type = each.value.instance_type + key_name = aws_key_pair.admin.id + availability_zone = var.availability_zone + subnet_id = each.value.public ? var.public_subnet_id : var.private_subnet_id + associate_public_ip_address = each.value.public + security_group_id = aws_security_group.instances.id + iam_instance_profile = each.value.iam_instance_profile + volume_type = each.value.volume_type + volume_size = each.value.volume_size + user_data = data.template_file.cloud_config.rendered + tags = merge( + var.tags, + { + "Name" = format("%s %s", title(var.name), title(each.value.name)) + "InstanceGroup" = each.value.name + } + ) +} + +resource "aws_security_group" "instances" { + name = local.instances_sg_name + vpc_id = var.network_id + + tags = merge( + var.tags, + { + "Name" = format("%s Instances", title(var.name)) + } + ) +} + +resource "aws_security_group_rule" "instances_vpc_ingress" { + security_group_id = aws_security_group.instances.id + type = "ingress" + protocol = -1 + from_port = 0 + to_port = 0 + cidr_blocks = [ + data.aws_vpc.target.cidr_block, + ] +} + +resource "aws_security_group_rule" "instances_open_egress" { + security_group_id = aws_security_group.instances.id + type = "egress" + protocol = -1 + from_port = 0 + to_port = 0 + cidr_blocks = [ + "0.0.0.0/0", + ] +} + +data "aws_vpc" "target" { + id = var.network_id +} diff --git a/terraform/modules/cluster/key-pairs.tf b/terraform/modules/cluster/key-pairs.tf new file mode 100644 index 00000000..1ebaa6ec --- /dev/null +++ b/terraform/modules/cluster/key-pairs.tf @@ -0,0 +1,33 @@ +resource "aws_key_pair" "admin" { + key_name = local.admin_key_name + public_key = tls_private_key.admin.public_key_openssh + + tags = merge( + var.tags, + { + "Name" = format("%s Simulator Admin", var.name) + } + ) +} + +resource "tls_private_key" "admin" { + algorithm = "RSA" + rsa_bits = 4096 +} + +resource "aws_key_pair" "player" { + key_name = local.player_key_name + public_key = tls_private_key.player.public_key_openssh + + tags = merge( + var.tags, + { + "Name" = format("%s Simulator User", var.name) + } + ) +} + +resource "tls_private_key" "player" { + algorithm = "RSA" + rsa_bits = 4096 +} \ No newline at end of file diff --git a/terraform/modules/cluster/locals.tf b/terraform/modules/cluster/locals.tf new file mode 100644 index 00000000..4ed41cb8 --- /dev/null +++ b/terraform/modules/cluster/locals.tf @@ -0,0 +1,32 @@ +locals { + bastion_sg_name = format("%s-bastion", var.name) + instances_sg_name = format("%s-instances", var.name) + admin_key_name = format("%s-admin-key", var.name) + player_key_name = format("%s-player-key", var.name) + + ssh_config_instances = merge([for i in module.instances : i.instances]...) + ssh_config_player = templatefile("${path.module}/templates/ssh_config", { + bastion_ip = aws_instance.bastion.public_ip, + ssh_user = "player" + ssh_force_tty = true + ssh_identity_file = var.ssh_identity_filename + ssh_known_hosts = var.ssh_known_hosts_filename + instances = {} + }) + ssh_config_admin = templatefile("${path.module}/templates/ssh_config", { + bastion_ip = aws_instance.bastion.public_ip, + ssh_user = "ubuntu" + ssh_force_tty = false + ssh_identity_file = var.ssh_identity_filename + ssh_known_hosts = var.ssh_known_hosts_filename + instances = local.ssh_config_instances + }) + + ansible_inventory_instances = merge([for i, g in var.instance_groups : { format("%ss", lower(var.instance_groups[i].name)) = keys(module.instances[i].instances) }]...) + ansible_config = templatefile("${path.module}/templates/ansible.cfg", { + ssh_config_filename = var.ssh_config_filename + }) + ansible_inventory = templatefile("${path.module}/templates/inventory.yaml", { + ansible_inventory_instances = local.ansible_inventory_instances + }) +} diff --git a/terraform/modules/cluster/modules/instance-group/instances.tf b/terraform/modules/cluster/modules/instance-group/instances.tf new file mode 100644 index 00000000..525ee28e --- /dev/null +++ b/terraform/modules/cluster/modules/instance-group/instances.tf @@ -0,0 +1,44 @@ +resource "aws_instance" "instance" { + count = var.instance_count + + ami = var.ami_id + instance_type = var.instance_type + key_name = var.key_name + availability_zone = var.availability_zone + subnet_id = var.subnet_id + associate_public_ip_address = var.associate_public_ip_address + hibernation = true + vpc_security_group_ids = [ + var.security_group_id, + ] + + # metadata_options { + # http_endpoint = local.http_endpoint + # } + + iam_instance_profile = var.iam_instance_profile + + root_block_device { + volume_type = var.volume_type + volume_size = var.volume_size + encrypted = true + } + + user_data = <