Skip to content

Commit

Permalink
feat: added --provisioners flag from score-compose (#37)
Browse files Browse the repository at this point in the history
* feat: added --provisioners flag from score-compose

Signed-off-by: Ben Meier <[email protected]>

* chore: readme updates

Signed-off-by: Ben Meier <[email protected]>

---------

Signed-off-by: Ben Meier <[email protected]>
  • Loading branch information
astromechza authored Sep 16, 2024
1 parent 034b3ba commit e2d158b
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 12 deletions.
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())
}
}

0 comments on commit e2d158b

Please sign in to comment.