Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added --provisioners flag from score-compose #37

Merged
merged 2 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ For details of how the standard "template" provisioner works, see the `template:

`score-k8s` comes with out-of-the-box support for:

| Type | Class | Params | Output |
| ------------- | ------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| volume | default | (none) | `source` |
| redis | default | (none) | `host`, `port`, `username`, `password` |
| postgres | default | (none) | `host`, `port`, `name` (aka `database`), `username`, `password` |
| mysql | default | (none) | `host`, `port`, `name` (aka `database`), `username`, `password` |
| dns | default | (none) | `host` |
| route | default | `host`, `path`, `port` | |
| Type | Class | Params | Output |
| ------------- | ------- | ---------------------- |-----------------------------------------------------------------|
| volume | default | (none) | `source` |
| redis | default | (none) | `host`, `port`, `username`, `password` |
| postgres | default | (none) | `host`, `port`, `name` (aka `database`), `username`, `password` |
| mysql | default | (none) | `host`, `port`, `name` (aka `database`), `username`, `password` |
| dns | default | (none) | `host` |
| route | default | `host`, `path`, `port` | |
| mongodb | default | (none) | `host`, `port`, `username`, `password`, `name`, `connection` |
| ampq | default | (nont) | `host`, `port`, `username`, `password`, `vhost` |

Users are encouraged to write their own custom provisioners to support new resource types or to modify the implementations above.

Expand All @@ -65,11 +67,17 @@ Examples:

# Initialise a new score-k8s project
score-k8s init
# Or disable the default score file generation if you already have a score file
score-k8s init --no-sample

# Optionally loading in provisoners from a remote url
score-k8s init --provisioners https://raw.githubusercontent.com/user/repo/main/example.yaml

Flags:
-f, --file string The score file to initialize (default "score.yaml")
-h, --help help for init
--no-sample Disable generation of the sample score file
-f, --file string The score file to initialize (default "score.yaml")
-h, --help help for init
--no-sample Disable generation of the sample score file
--provisioners stringArray A provisioners file to install. May be specified multiple times. Supports http://host/file, https://host/file, git-ssh://git@host/repo.git/file, and git-https://host/repo.git/file formats.
```

### Generate
Expand Down
46 changes: 46 additions & 0 deletions internal/provisioners/loader/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,17 @@ package loader

import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"log/slog"
"math"
"net/url"
"os"
"path/filepath"
"strings"
"time"

"gopkg.in/yaml.v3"

Expand Down Expand Up @@ -92,3 +97,44 @@ func LoadProvisionersFromDirectory(path string, suffix string) ([]provisioners.P
}
return out, nil
}

// SaveProvisionerToDirectory saves the provisioner content (data) from the provisionerUrl to a new provisioners file
// in the path directory.
func SaveProvisionerToDirectory(path string, provisionerUrl string, data []byte) error {
// First validate whether this file contains valid provisioner data.
if _, err := LoadProvisioners(data); err != nil {
return fmt.Errorf("invalid provisioners file: %w", err)
}
// Append a heading indicating the source and time
data = append([]byte(fmt.Sprintf("# Downloaded from %s at %s\n", provisionerUrl, time.Now())), data...)
hashValue := sha256.Sum256([]byte(provisionerUrl))
hashName := base64.RawURLEncoding.EncodeToString(hashValue[:16]) + DefaultSuffix
// We use a time prefix to always put the most recently downloaded files first lexicographically. So subtract
// time from uint64 and convert it into a base64 two's complement binary representation.
timePrefix := base64.RawURLEncoding.EncodeToString(binary.BigEndian.AppendUint64([]byte{}, uint64(math.MaxInt64-time.Now().UnixNano())))

targetPath := filepath.Join(path, timePrefix+"."+hashName)
tmpPath := targetPath + ".tmp"
if err := os.WriteFile(tmpPath, data, 0600); err != nil {
return fmt.Errorf("failed to write file: %w", err)
} else if err := os.Rename(tmpPath, targetPath); err != nil {
return fmt.Errorf("failed to rename temp file: %w", err)
}
slog.Info(fmt.Sprintf("Wrote provisioner from '%s' to %s", provisionerUrl, targetPath))

// Remove any old files that have the same source.
if items, err := os.ReadDir(path); err != nil {
return err
} else {
for _, item := range items {
if strings.HasSuffix(item.Name(), hashName) && !strings.HasPrefix(item.Name(), timePrefix) {
if err := os.Remove(filepath.Join(path, item.Name())); err != nil {
return fmt.Errorf("failed to remove old copy of provisioner loaded from '%s': %w", provisionerUrl, err)
}
slog.Debug(fmt.Sprintf("Removed old copy of provisioner loaded from '%s'", provisionerUrl))
}
}
}

return nil
}
37 changes: 36 additions & 1 deletion main_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,27 @@
package main

import (
"fmt"
"log/slog"
"os"
"path/filepath"

"github.com/pkg/errors"
"github.com/score-spec/score-go/framework"
scoretypes "github.com/score-spec/score-go/types"
"github.com/score-spec/score-go/uriget"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"

"github.com/score-spec/score-k8s/internal/project"
"github.com/score-spec/score-k8s/internal/provisioners/default"
"github.com/score-spec/score-k8s/internal/provisioners/loader"
)

const (
initCmdFileFlag = "file"
initCmdFileNoSampleFlag = "no-sample"
initCmdProvisionerFlag = "provisioners"
)

var initCmd = &cobra.Command{
Expand All @@ -43,10 +47,20 @@ empty state and default provisioners file into the '.score-k8s' subdirectory.

The '.score-k8s' directory contains state that will be used to generate any Kubernetes resource manifests including
potentially sensitive data and raw secrets, so this should not be checked into generic source control.

Custom provisioners can be installed by uri using the --provisioners flag. The provisioners will be installed and take
precedence in the order they are defined over the default provisioners. If init has already been called with provisioners
the new provisioners will take precedence.
`,
Example: `
# Initialise a new score-k8s project
score-k8s init`,
score-k8s init

# Or disable the default score file generation if you already have a score file
score-k8s init --no-sample

# Optionally loading in provisoners from a remote url
score-k8s init --provisioners https://raw.githubusercontent.com/user/repo/main/example.yaml`,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
Expand Down Expand Up @@ -126,13 +140,34 @@ potentially sensitive data and raw secrets, so this should not be checked into g
slog.Info("Skipping creation of initial Score file since it already exists", "file", initCmdScoreFile)
}

if v, _ := cmd.Flags().GetStringArray(initCmdProvisionerFlag); len(v) > 0 {
for i, vi := range v {
data, err := uriget.GetFile(cmd.Context(), vi)
if err != nil {
return fmt.Errorf("failed to load provisioner %d: %w", i+1, err)
}
if err := loader.SaveProvisionerToDirectory(sd.Path, vi, data); err != nil {
return fmt.Errorf("failed to save provisioner %d: %w", i+1, err)
}
}
}

if provs, err := loader.LoadProvisionersFromDirectory(sd.Path, loader.DefaultSuffix); err != nil {
return fmt.Errorf("failed to load existing provisioners: %w", err)
} else {
slog.Debug(fmt.Sprintf("Successfully loaded %d resource provisioners", len(provs)))
}

slog.Info(fmt.Sprintf("Read more about the Score specification at https://docs.score.dev/docs/"))

return nil
},
}

func init() {
initCmd.Flags().StringP(initCmdFileFlag, "f", "score.yaml", "The score file to initialize")
initCmd.Flags().Bool(initCmdFileNoSampleFlag, false, "Disable generation of the sample score file")
initCmd.Flags().StringArray(initCmdProvisionerFlag, nil, "A provisioners file to install. May be specified multiple times. Supports http://host/file, https://host/file, git-ssh://git@host/repo.git/file, and git-https://host/repo.git/file formats.")

rootCmd.AddCommand(initCmd)
}
34 changes: 34 additions & 0 deletions main_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/score-spec/score-k8s/internal/project"
"github.com/score-spec/score-k8s/internal/provisioners/loader"
)

func TestInitNominal(t *testing.T) {
Expand Down Expand Up @@ -120,3 +121,36 @@ func TestInitNominal_run_twice(t *testing.T) {
assert.Equal(t, map[string]interface{}{}, sd.State.SharedState)
}
}

func TestInitWithProvisioners(t *testing.T) {
td := t.TempDir()
wd, _ := os.Getwd()
require.NoError(t, os.Chdir(td))
defer func() {
require.NoError(t, os.Chdir(wd))
}()

td2 := t.TempDir()
assert.NoError(t, os.WriteFile(filepath.Join(td2, "one.provisioners.yaml"), []byte(`
- uri: template://one
type: thing
outputs: "{}"
`), 0644))
assert.NoError(t, os.WriteFile(filepath.Join(td2, "two.provisioners.yaml"), []byte(`
- uri: template://two
type: thing
outputs: "{}"
`), 0644))

stdout, stderr, err := executeAndResetCommand(context.Background(), rootCmd, []string{"init", "--provisioners", filepath.Join(td2, "one.provisioners.yaml"), "--provisioners", "file://" + filepath.Join(td2, "two.provisioners.yaml")})
assert.NoError(t, err)
assert.Equal(t, "", stdout)
assert.NotEqual(t, "", strings.TrimSpace(stderr))

provs, err := loader.LoadProvisionersFromDirectory(filepath.Join(td, ".score-k8s"), loader.DefaultSuffix)
assert.NoError(t, err)
if assert.Greater(t, len(provs), 2) {
assert.Equal(t, "template://two", provs[0].Uri())
assert.Equal(t, "template://one", provs[1].Uri())
}
}
Loading