feat: CLI interface created with clir

This commit is contained in:
Dan Anglin 2021-06-30 06:03:02 +01:00
parent dd473a9808
commit ed6afb94a1
Signed by: dananglin
GPG key ID: 0C1D44CFBEE68638
11 changed files with 478 additions and 135 deletions

139
cmd/helix/main.go Normal file
View file

@ -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
}

1
go.mod
View file

@ -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
)

2
go.sum
View file

@ -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=

40
internal/config/config.go Normal file
View file

@ -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
}

View file

@ -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
}
}

103
internal/stacks/manage.go Normal file
View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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 }}

View file

@ -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