Skip to content

Commit

Permalink
feat: support validating and installing provisioner files by uri
Browse files Browse the repository at this point in the history
  • Loading branch information
astromechza committed Sep 11, 2024
1 parent 4376746 commit 4a02575
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 9 deletions.
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,25 @@ acts as a namespace when multiple score files and containers are used.
Usage:
score-compose init [flags]
Examples:
# Define a score file to generate
score-compose init --file score2.yaml
# Or override the docker compose project name
score-compose init --project score-compose2
# Or disable the default score file generation if you already have a score file
score-compose init --no-sample
# Optionally loading in provisoners from a remote url
score-compose init --provisioner 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
-p, --project string Set the name of the docker compose project (defaults to the current directory name)
-f, --file string The score file to initialize (default "./score.yaml")
-h, --help help for init
-p, --project string Set the name of the docker compose project (defaults to the current directory name)
--provisioner stringArray A provisioner file to install. Supports http://host/file, https://host/file, git-ssh://git@host/repo.git/file, and git-https://host/repo.git/file formats.
Global Flags:
--quiet Mute any logging output
Expand Down
27 changes: 25 additions & 2 deletions examples/06-resource-provisioning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ ports:

## The `*.provisioners.yaml` files

When you run `score-compose init`, a [99-default.provisioners.yaml](https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml) file is created, which is a YAML file holding the definition of the built-in provisioners.
When you run `score-compose init`, a [zz-default.provisioners.yaml](https://github.com/score-spec/score-compose/blob/main/internal/command/default.provisioners.yaml) file is created, which is a YAML file holding the definition of the built-in provisioners.

When you run `score-compose generate`, all `*.provisioners.yaml` files are loaded in lexicographic order from the `.score-compose` directory. This allows projects to include their own custom provisioners that extend or override the defaults.

Expand All @@ -143,7 +143,30 @@ Each entry in the file has the following common fields, other fields may also ex
id: <optional resource id>
```

The uri of each provisioner is a combination of it's implementation (either `template://` or `cmd://`) and a unique identifier.
The uri of each provisioner is a combination of its implementation (either `template://` or `cmd://`) and a unique identifier.
Provisioners are matched in first-match order when loading the provisioner files lexicographically, so any custom provisioner
files are matched first before `zz-default.provisioners.yaml`.

### Installing provisioner files

To easily install provisioners, `score-compose` provides the `--provisioner` flag for `init`, which downloads the provisioner
file via a URL and installs it with the highest priority.

For example, when running the following, the provisioners file B will be matched before A because B was installed after A:

```
score-compose init --provisioner https://example.com/provisioner-A.yaml --provisioners https://example.com/provisioner-B.yaml
```

The provisioners can be loaded from the following kinds of urls:

- Files: `./a/relative/path.provisioners.yaml`, `/an/absolute/path.provisioners.yaml`, `file://a/file/uri.provisioners.yaml`.
- Http: `http://example.com/a.provisioners.yaml`, `https://example.com/b.provisioners.yaml`
- Git over ssh: `git-ssh://git@github.com/user/repo.git/common.provisioners.yaml`
- Git over http: `git-http://github.com/user/repo.git/common.provisioners.yaml`

This is commonly used to import custom provisioners or common provisioners used by your team or organization and supported
by your platform.

### The `template://` provisioner

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/compose-spec/compose-go/v2 v2.1.3
github.com/imdario/mergo v0.3.16
github.com/mitchellh/mapstructure v1.5.0
github.com/score-spec/score-go v1.7.2
github.com/score-spec/score-go v1.8.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/score-spec/score-go v1.7.2 h1:kRqm7506pAhJVMIfZDkfhOAoIedm5A3IfjTjtoBOyRg=
github.com/score-spec/score-go v1.7.2/go.mod h1:3QSPH5QVMIX7FdhktLLFtjLQTL1/ENqrWDe5lSdZGFc=
github.com/score-spec/score-go v1.8.0 h1:Dc2Hbz7pONKgsVjjSeIgmciGpG7PojkqlGtKwgmkAJA=
github.com/score-spec/score-go v1.8.0/go.mod h1:3QSPH5QVMIX7FdhktLLFtjLQTL1/ENqrWDe5lSdZGFc=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
Expand Down
24 changes: 23 additions & 1 deletion internal/command/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"strings"

"github.com/score-spec/score-go/framework"
"github.com/score-spec/score-go/uriget"
"github.com/spf13/cobra"

"github.com/score-spec/score-compose/internal/project"
Expand Down Expand Up @@ -70,6 +71,7 @@ resources: {}
initCmdFileFlag = "file"
initCmdFileProjectFlag = "project"
initCmdFileNoSampleFlag = "no-sample"
initCmdProvisionerFlag = "provisioner"
)

//go:embed default.provisioners.yaml
Expand All @@ -87,6 +89,10 @@ not be checked into source control. Add it to your .gitignore file if you use Gi
The project name will be used as a Docker compose project name when the final compose files are written. This name
acts as a namespace when multiple score files and containers are used.
Custom provisioners can be installed by uri using the --provisioner 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: `
# Define a score file to generate
Expand All @@ -96,7 +102,10 @@ acts as a namespace when multiple score files and containers are used.
score-compose init --project score-compose2
# Or disable the default score file generation if you already have a score file
score-compose init --no-sample`,
score-compose init --no-sample
# Optionally loading in provisoners from a remote url
score-compose init --provisioner https://raw.githubusercontent.com/user/repo/main/example.yaml`,

// don't print the errors - we print these ourselves in main()
SilenceErrors: true,
Expand Down Expand Up @@ -188,6 +197,18 @@ acts as a namespace when multiple score files and containers are used.
slog.Info(fmt.Sprintf("Found existing Score file '%s'", 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 {
Expand All @@ -204,6 +225,7 @@ func init() {
initCmd.Flags().StringP(initCmdFileFlag, "f", scoreFileDefault, "The score file to initialize")
initCmd.Flags().StringP(initCmdFileProjectFlag, "p", "", "Set the name of the docker compose project (defaults to the current directory name)")
initCmd.Flags().Bool(initCmdFileNoSampleFlag, false, "Disable generation of the sample score file")
initCmd.Flags().StringArray(initCmdProvisionerFlag, nil, "A provisioner file to install. 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)
}
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
}

0 comments on commit 4a02575

Please sign in to comment.