From ed6afb94a11771a5c7c57465311cec7fa3160e25 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Wed, 30 Jun 2021 06:03:02 +0100 Subject: [PATCH] feat: CLI interface created with clir --- cmd/helix/main.go | 139 ++++++++++++++++++ go.mod | 1 + go.sum | 2 + internal/config/config.go | 40 +++++ internal/stacks/actions.go | 0 internal/stacks/docker_network.go | 86 +++++++++++ internal/stacks/manage.go | 103 +++++++++++++ internal/stacks/network/deploy.go | 25 ---- internal/stacks/network/stack.go | 110 -------------- .../stacks/templates/gitea/Dockerfile.tmpl | 15 ++ internal/stacks/templates/gitea/app.ini.tmpl | 92 ++++++++++++ 11 files changed, 478 insertions(+), 135 deletions(-) create mode 100644 cmd/helix/main.go create mode 100644 internal/config/config.go delete mode 100644 internal/stacks/actions.go create mode 100644 internal/stacks/docker_network.go create mode 100644 internal/stacks/manage.go delete mode 100644 internal/stacks/network/deploy.go delete mode 100644 internal/stacks/network/stack.go create mode 100644 internal/stacks/templates/gitea/Dockerfile.tmpl create mode 100644 internal/stacks/templates/gitea/app.ini.tmpl diff --git a/cmd/helix/main.go b/cmd/helix/main.go new file mode 100644 index 0000000..eeaf0c2 --- /dev/null +++ b/cmd/helix/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/leaanthony/clir" + "gitlab.com/dananglin/forge-platform/internal/config" + "gitlab.com/dananglin/forge-platform/internal/stacks" +) + +func main() { + if err := run(); err != nil { + fmt.Printf("ERROR: %v.\n", err) + os.Exit(1) + } +} + +func run() error { + helix := clir.NewCli("helix", "A CLI tool for managing your forge platform.", "v0.0.1") + helix.LongDescription(`Helix is a command line tool used to help manage your forge platform. +This uses the Pulumi Automation API library to create and manage different components +that builds your forge platform on a Linode instance.`) + + versionCmd := helix.NewSubCommand("version", "Print the version of this application.") + versionCmd.Action(versionFunc(helix.Version())) + + var file string + var stack string + + ctx := context.Background() + + updateCmd := helix.NewSubCommand("update", "Update a stack.") + updateCmd.Action(updateFunc(ctx, &file, &stack)) + updateCmd.StringFlag("file", "the path to the configuration file", &file) + updateCmd.StringFlag("stack", "the name of the stack", &stack) + + previewCmd := helix.NewSubCommand("preview", "Preview upcoming changes to a stack.") + previewCmd.Action(previewFunc(ctx, &file, &stack)) + previewCmd.StringFlag("file", "the path to the configuration file", &file) + previewCmd.StringFlag("stack", "the name of the stack", &stack) + + destroyCmd := helix.NewSubCommand("destroy", "Destroy a stack.") + destroyCmd.Action(destroyFunc(ctx, &file, &stack)) + destroyCmd.StringFlag("file", "the path to the configuration file", &file) + destroyCmd.StringFlag("stack", "the name of the stack", &stack) + + helix.DefaultCommand(versionCmd) + + return helix.Run() +} + +func versionFunc(version string) clir.Action { + return func() error { + fmt.Printf("helix version %s\n", version) + + return nil + } +} + +func previewFunc(ctx context.Context, file, stack *string) clir.Action { + flagMap := map[string]*string{ + "file": file, + "stack": stack, + } + + return func() error { + if err := checkFlags(flagMap); err != nil { + return err + } + + c, err := config.NewConfig(*flagMap["file"]) + if err != nil { + return fmt.Errorf("unable to get configuration...\n%v", err) + } + + previewer, err := stacks.NewPreviewer(ctx, c.ProjectName, *stack, c) + if err != nil { + return err + } + + if err := previewer.Preview(ctx); err != nil { + return err + } + + return nil + } +} + +func updateFunc(ctx context.Context, file, stack *string) clir.Action { + flagMap := map[string]*string{ + "file": file, + "stack": stack, + } + + return func() error { + if err := checkFlags(flagMap); err != nil { + return err + } + + // parse json configuration (config package) + // create the updater (interface in stacks) (all logic in stacks) + // run the previewer + // return + + return nil + } +} + +func destroyFunc(ctx context.Context, file, stack *string) clir.Action { + flagMap := map[string]*string{ + "file": file, + "stack": stack, + } + + return func() error { + if err := checkFlags(flagMap); err != nil { + return err + } + + // parse json configuration (config package) + // create the destroyer (interface in stacks) (all logic in stacks) + // run the previewer + // return + + return nil + } +} + +func checkFlags(f map[string]*string) error { + for k, v := range f { + if len(*v) == 0 { + return fmt.Errorf("the value for the '%s' flag is not set", k) + } + } + + return nil +} diff --git a/go.mod b/go.mod index ffc0d7b..6485852 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitlab.com/dananglin/forge-platform go 1.16 require ( + github.com/leaanthony/clir v1.0.4 github.com/pulumi/pulumi-docker/sdk/v3 v3.0.0 github.com/pulumi/pulumi/sdk/v3 v3.4.0 ) diff --git a/go.sum b/go.sum index 5d110dc..cfd3a32 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leaanthony/clir v1.0.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU= +github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..f25790d --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,40 @@ +package config + +import ( + "encoding/json" + "fmt" + "io/ioutil" +) + +type Config struct { + ProjectName string `json:"project"` + Docker DockerConfig `json:"docker"` +} + +type DockerConfig struct { + Network DockerNetworkConfig `json:"network"` +} + +// DockerNetworkStackArgs contains arguments for +// creating the DockerNetworkStack +type DockerNetworkConfig struct { + Name string `json:"name"` + Subnet string `json:"subnet"` + Driver string `json:"driver"` +} + +func NewConfig(file string) (Config, error) { + var c Config + var err error + + data, err := ioutil.ReadFile(file) + if err != nil { + return c, fmt.Errorf("unable to read data from %s...\n%v", file, err) + } + + if err = json.Unmarshal(data, &c); err != nil { + return c, fmt.Errorf("unable to decode the JSON configuration from %s...\n%v", file, err) + } + + return c, nil +} diff --git a/internal/stacks/actions.go b/internal/stacks/actions.go deleted file mode 100644 index e69de29..0000000 diff --git a/internal/stacks/docker_network.go b/internal/stacks/docker_network.go new file mode 100644 index 0000000..5a51fc7 --- /dev/null +++ b/internal/stacks/docker_network.go @@ -0,0 +1,86 @@ +package stacks + +import ( + "context" + "fmt" + "os" + + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optdestroy" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optpreview" + "github.com/pulumi/pulumi/sdk/v3/go/auto/optup" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + "gitlab.com/dananglin/forge-platform/internal/config" + "gitlab.com/dananglin/forge-platform/internal/docker" +) + +// DockerNetworkStack is a stack for +// managing the Docker network. +type DockerNetworkStack struct { + Name string + Stack auto.Stack +} + +// newDockerNetworkStack creates a new DockerNetworkStack value. +func newDockerNetworkStack(ctx context.Context, project, stack string, args config.DockerNetworkConfig) (*DockerNetworkStack, error) { + + deployFunc := deployDockerNetworkStack(args.Name, args.Subnet, args.Driver) + + s, err := createOrSelectStack(ctx, project, stack, deployFunc) + if err != nil { + return nil, fmt.Errorf("unable to initialise the '%s' stack...\n%w", stack, err) + } + + n := DockerNetworkStack{ + Name: stack, + Stack: s, + } + + return &n, nil +} + +func (d *DockerNetworkStack) Preview(ctx context.Context) error { + stdoutStreamer := optpreview.ProgressStreams(os.Stdout) + _, err := d.Stack.Preview(ctx, stdoutStreamer) + if err != nil { + return fmt.Errorf("unable to preview the '%s' stack...\n%w", d.Name, err) + } + + return nil +} + +func (d *DockerNetworkStack) Update(ctx context.Context) error { + stdoutStreamer := optup.ProgressStreams(os.Stdout) + _, err := d.Stack.Up(ctx, stdoutStreamer) + if err != nil { + return fmt.Errorf("unable to update the '%s' stack...\n%w", d.Name, err) + } + + return nil +} + +func (d *DockerNetworkStack) Destroy(ctx context.Context) error { + stdoutStreamer := optdestroy.ProgressStreams(os.Stdout) + _, err := d.Stack.Destroy(ctx, stdoutStreamer) + if err != nil { + return fmt.Errorf("unable to destroy the '%s' stack...\n%w", d.Name, err) + } + return nil +} + +// DeployDockerNetworkStack returns a Pulumi run function +// that deploys the Docker network stack. +func deployDockerNetworkStack(name, subnet, driver string) pulumi.RunFunc { + return func(ctx *pulumi.Context) error { + config := docker.DockerNetworkConfig{ + Name: pulumi.String(name), + Subnet: pulumi.String(subnet), + Driver: pulumi.String(driver), + } + + if err := docker.CreateNetwork(ctx, config); err != nil { + return fmt.Errorf("unable to complete the deployment...\n%w", err) + } + return nil + } +} diff --git a/internal/stacks/manage.go b/internal/stacks/manage.go new file mode 100644 index 0000000..ac21856 --- /dev/null +++ b/internal/stacks/manage.go @@ -0,0 +1,103 @@ +package stacks + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/pulumi/pulumi/sdk/v3/go/auto" + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" + "github.com/pulumi/pulumi/sdk/v3/go/common/workspace" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" + + "gitlab.com/dananglin/forge-platform/internal/config" +) + +const ( + dockerNetworkStackName string = "docker_network" +) + +type Previewer interface { + Preview(ctx context.Context) error +} + +type Updater interface { + Update(ctx context.Context) error +} + +type Destroyer interface { + Destroy(ctx context.Context) error +} + +func createOrSelectStack(ctx context.Context, projectName, stackName string, deployFunc pulumi.RunFunc) (auto.Stack, error) { + var s auto.Stack + + opts, err := workspaceOptions(projectName, stackName) + if err != nil { + return s, fmt.Errorf("unable to create the workspace options...\n%w", err) + } + + fmt.Printf("Creating/selecting stack (%s)...\n", stackName) + s, err = auto.UpsertStackInlineSource(ctx, stackName, projectName, deployFunc, opts...) + if err != nil { + return s, fmt.Errorf("unable to create/select the stack...\n%w", err) + } + + fmt.Printf("Refreshing stack (%s)...\n", stackName) + _, err = s.Refresh(ctx) + if err != nil { + return s, fmt.Errorf("unable to refresh the stack...\n%w", err) + } + + return s, nil +} + +func workspaceOptions(projectName, stackName string) ([]auto.LocalWorkspaceOption, error) { + pulumiHome := auto.PulumiHome(filepath.Join(os.Getenv("HOME"), ".pulumi")) + + wd := filepath.Join(os.Getenv("HOME"), "Pulumi", "projects", projectName, "workspaces", stackName) + + fmt.Printf("Ensuring that %s exists...\n", wd) + if err := os.MkdirAll(wd, 0750); err != nil { + return nil, fmt.Errorf("unable to ensure that the directory exists...\n%w", err) + } + + workDir := auto.WorkDir(wd) + + secretsProvider := auto.SecretsProvider("passphrase") + + backend := filepath.Join(os.Getenv("HOME"), "Pulumi", "projects", projectName, "stacks") + if err := os.MkdirAll(backend, 0750); err != nil { + return nil, fmt.Errorf("unable to create the backend directory...\n%w", err) + } + + project := auto.Project(workspace.Project{ + Name: tokens.PackageName(projectName), + Runtime: workspace.NewProjectRuntimeInfo("go", nil), + Backend: &workspace.ProjectBackend{ + URL: fmt.Sprintf("file://%s", backend), + }, + }) + + opts := []auto.LocalWorkspaceOption{pulumiHome, secretsProvider, project, workDir} + + return opts, nil +} + +func NewPreviewer(ctx context.Context, project, stack string, c config.Config) (Previewer, error) { + var p Previewer + var err error + + switch stack { + case dockerNetworkStackName: + p, err = newDockerNetworkStack(ctx, project, stack, c.Docker.Network) + if err != nil { + return nil, fmt.Errorf("unable to initialise '%s' stack...\n%v", stack, err) + } + default: + return nil, fmt.Errorf("unknown stack name '%s'", stack) + } + + return p, nil +} diff --git a/internal/stacks/network/deploy.go b/internal/stacks/network/deploy.go deleted file mode 100644 index 537bd88..0000000 --- a/internal/stacks/network/deploy.go +++ /dev/null @@ -1,25 +0,0 @@ -package network - -import ( - "fmt" - - "github.com/pulumi/pulumi/sdk/v3/go/pulumi" - "gitlab.com/dananglin/forge-platform/internal/docker" -) - -// DeployDockerNetworkStack returns a Pulumi run function -// that deploys the Docker network stack. -func DeployDockerNetworkStack(name, subnet, driver string) pulumi.RunFunc { - return func(ctx *pulumi.Context) error { - config := docker.DockerNetworkConfig{ - Name: pulumi.String(name), - Subnet: pulumi.String(subnet), - Driver: pulumi.String(driver), - } - - if err := docker.CreateNetwork(ctx, config); err != nil { - return fmt.Errorf("unable to deploy the Docker Network Stack...\n%w", err) - } - return nil - } -} diff --git a/internal/stacks/network/stack.go b/internal/stacks/network/stack.go deleted file mode 100644 index 7be5efc..0000000 --- a/internal/stacks/network/stack.go +++ /dev/null @@ -1,110 +0,0 @@ -package network - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/pulumi/pulumi/sdk/v3/go/auto" - "github.com/pulumi/pulumi/sdk/v3/go/auto/optdestroy" - "github.com/pulumi/pulumi/sdk/v3/go/auto/optpreview" - "github.com/pulumi/pulumi/sdk/v3/go/auto/optup" - "github.com/pulumi/pulumi/sdk/v3/go/pulumi" -) - -const ( - actionPreview string = "preview" - actionUpdate string = "update" - actionDestroy string = "destroy" - - stack string = "dockerNetwork" -) - -// DockerNetworkStackArgs contains arguments for -// creating the DockerNetworkStack -type DockerNetworkStackArgs struct { - NetworkName string - Subnet string - NetworkDriver string -} - -// DockerNetworkStack is a stack for -// managing the Docker network. -type DockerNetworkStack struct { - Name string - Stack auto.Stack -} - -// NewDockerNetworkStack creates a new DockerNetworkStack value. -func NewDockerNetworkStack(ctx context.Context, project string, args DockerNetworkStackArgs, opts ...auto.LocalWorkspaceOption) (*DockerNetworkStack, error) { - - deployFunc := DeployDockerNetworkStack(args.NetworkName, args.Subnet, args.NetworkDriver) - - s, err := createOrSelectStack(ctx, project, stack, deployFunc, opts...) - if err != nil { - return nil, fmt.Errorf("unable to initialise stack (%s)...\n%w", stack, err) - } - - n := DockerNetworkStack{ - Name: stack, - Stack: s, - } - - return &n, nil -} - -// Process performs an action on the DockerNetworkStack. -func (n *DockerNetworkStack) Process(ctx context.Context, action string) error { - switch action { - case actionPreview: - stdoutStreamer := optpreview.ProgressStreams(os.Stdout) - _, err := n.Stack.Preview(ctx, stdoutStreamer) - if err != nil { - return fmt.Errorf("unable to preview the stack...\n%w", err) - } - case actionUpdate: - stdoutStreamer := optup.ProgressStreams(os.Stdout) - _, err := n.Stack.Up(ctx, stdoutStreamer) - if err != nil { - return fmt.Errorf("unable to update the stack...\n%w", err) - } - case actionDestroy: - stdoutStreamer := optdestroy.ProgressStreams(os.Stdout) - _, err := n.Stack.Destroy(ctx, stdoutStreamer) - if err != nil { - return fmt.Errorf("unable to destroy the stack...\n%w", err) - } - default: - return fmt.Errorf("unknown action '%s'", action) - } - - return nil -} - -func createOrSelectStack(ctx context.Context, project, stack string, deployFunc pulumi.RunFunc, opts ...auto.LocalWorkspaceOption) (auto.Stack, error) { - wd := filepath.Join(os.Getenv("HOME"), "Pulumi", "projects", project, "workspaces", stack) - - fmt.Printf("INFO: Ensuring that %s exists...\n", wd) - if err := os.MkdirAll(wd, 0750); err != nil { - return auto.Stack{}, fmt.Errorf("unable to ensure that the directory exists...\n%w", err) - } - - workDir := auto.WorkDir(wd) - - opts = append(opts, workDir) - - fmt.Printf("INFO: Creating/selecting stack (%s)...\n", stack) - s, err := auto.UpsertStackInlineSource(ctx, stack, project, deployFunc, opts...) - if err != nil { - return auto.Stack{}, fmt.Errorf("unable to create/select the stack...\n%w", err) - } - - fmt.Printf("INFO: Refreshing stack (%s)...\n", stack) - _, err = s.Refresh(ctx) - if err != nil { - return auto.Stack{}, fmt.Errorf("unable to refresh the stack...\n%w", err) - } - - return s, nil -} diff --git a/internal/stacks/templates/gitea/Dockerfile.tmpl b/internal/stacks/templates/gitea/Dockerfile.tmpl new file mode 100644 index 0000000..efe198e --- /dev/null +++ b/internal/stacks/templates/gitea/Dockerfile.tmpl @@ -0,0 +1,15 @@ +{{/* vim: set ft=dockerfile : */}} + +ARG GITEA_VERSION={{ .Version }} + +FROM gitea/gitea:${GITEA_VERSION} + +ENV USER_UID=1000 \ + USER_GID=1000 \ + GITEA_CUSTOM=/helix/gitea/custom + +ADD files app.ini /helix/gitea/custom/app.ini + +RUN chown -R ${USER_ID}:${USER_GID} /helix + +EXPOSE {{ .App.HttpPort }} {{ .App.SshPort }} diff --git a/internal/stacks/templates/gitea/app.ini.tmpl b/internal/stacks/templates/gitea/app.ini.tmpl new file mode 100644 index 0000000..d7557b3 --- /dev/null +++ b/internal/stacks/templates/gitea/app.ini.tmpl @@ -0,0 +1,92 @@ +{{/* vim: set ft=dosini : */}} +APP_NAME = {{ .App.Name }} +RUN_MODE = {{ .App.RunMode }} + +[repository] +ROOT = /data/gitea/repositories +DEFAULT_BRANCH = main + +[repository.local] +LOCAL_COPY_PATH = /data/gitea/tmp/local-repo + +[repository.upload] +TEMP_PATH = /data/gitea/uploads + +[repository.signing] +; Gitea will sign initial commits only if the user has a public key. +INITIAL_COMMIT = pubkey + +[ui] +DEFAULT_THEME = arc-green + +[server] +APP_DATA_PATH = /data/gitea +DOMAIN = {{ .App.Domain }} +HTTP_ADDR = {{ .Container.Ip }} +HTTP_PORT = {{ .App.HttpPort }} +ROOT_URL = {{ .App.RootUrl }} +DISABLE_SSH = false +SSH_DOMAIN = {{ .App.SshDomain }} +SSH_PORT = {{ .App.SshPort }} +SSH_LISTEN_PORT = {{ .App.SshPort }} +LFS_START_SERVER = false +LFS_CONTENT_PATH = /data/gitea/lfs + +[ssh.minimum_key_sizes] +ED25519 = 256 +ECDSA = 256 +RSA = 4096 +DSA = -1 + +[database] +DB_TYPE = sqlite3 +PATH = /data/gitea/database/gitea.db + +[indexer] +ISSUE_INDEXER_PATH = /data/gitea/indexers/issues.bleve + +[session] +PROVIDER_CONFIG = /data/gitea/sessions + +[queue] +DATADIR = /data/gitea/queues + +[admin] +DISABLE_REGULAR_ORG_CREATION = true +DEFAULT_EMAIL_NOTIFICATION = disabled + +[security] +INSTALL_LOCK = true +SECRET_KEY = {{ .App.SecretKey }} +LOGIN_REMEMBER_DAYS = 1 +MIN_PASSWORD_LENGTH = 12 +PASSWORD_COMPLEXITY = lower,upper,digit + +[service] +DISABLED_REGISTRATION = true +REQUIRE_SIGNIN_VIEW = false + +[picture] +AVATAR_UPLOAD_PATH = /data/gitea/avatars +REPOSITORY_AVATAR_UPLOAD_PATH = /data/gitea/repo-avatars + +[attachment] +ENABLED = true +PATH = /data/gitea/attachments + +[log] +ROOT_PATH = /data/gitea/log +MODE = console +LEVEL = {{ .App.LogLevel }} + +[log.console] +STDERR = false + +[i18n] +LANGS = en-US +NAMES = English + +[other] +SHOW_FOOTER_BRANDING = true +SHOW_FOOTER_VERSION = false +SHOW_FOOTER_TEMPLATE_LOAD_TIME = false