feat: recipe sync

This commit is contained in:
decentral1se 2021-08-10 12:21:42 +02:00
parent c6ea18311e
commit 83671f42a2
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
4 changed files with 220 additions and 8 deletions

View File

@ -40,7 +40,7 @@
- [x] `ls`
- [x] `create`
- [x] `upgrade`
- [ ] `sync` (in-progress `@decentral1se`)
- [x] `sync`
- [x] `versions`
- [x] `lint`
- [ ] `upgrade`

View File

@ -271,15 +271,59 @@ This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
}
var recipeSyncCommand = &cli.Command{
Name: "sync",
Usage: "Generate recipe labels and publish tags",
Name: "sync",
Usage: "Generate recipe labels",
Description: `
This command will generate labels for each service which correspond to the
following format:
coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST}
The <recipe> configuration will be updated on the local file system. These
labels are consumed by abra in other command invocations and used to determine
the versioning metadata of up-and-running containers are.
`,
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := c.Args().First()
if recipe == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
}
// TODO: part 2 of https://git.coopcloud.tech/coop-cloud/go-abra/issues/39#issuecomment-8066
compose, err := config.GetAppComposeFiles(recipe)
if err != nil {
logrus.Fatal(err)
}
hasAppService := false
for _, service := range compose.Services {
if service.Name == "app" {
hasAppService = true
}
}
if !hasAppService {
logrus.Fatal(fmt.Sprintf("No 'app' service defined in '%s', cannot proceed", recipe))
}
for _, service := range compose.Services {
img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
digest, err := client.GetTagDigest(img)
if err != nil {
logrus.Fatal(err)
}
tag := img.(reference.NamedTagged).Tag()
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest)
if err := config.UpdateAppComposeLabel(recipe, service.Name, label); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}

View File

@ -1,22 +1,28 @@
package client
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"
"coopcloud.tech/abra/web"
"github.com/docker/distribution/reference"
)
type Tag struct {
type RawTag struct {
Layer string
Name string
}
type Tags []Tag
type RawTags []RawTag
var registryURL = "https://registry.hub.docker.com/v1/repositories/%s/tags"
func GetRegistryTags(image string) (Tags, error) {
var tags Tags
func GetRegistryTags(image string) (RawTags, error) {
var tags RawTags
tagsUrl := fmt.Sprintf(registryURL, image)
if err := web.ReadJSON(tagsUrl, &tags); err != nil {
@ -25,3 +31,119 @@ func GetRegistryTags(image string) (Tags, error) {
return tags, nil
}
// getRegv2Token retrieves a registry v2 authentication token.
func getRegv2Token(image reference.Named) (string, error) {
img := reference.Path(image.(reference.Named))
authTokenURL := fmt.Sprintf("https://auth.docker.io/token?service=registry.docker.io&scope=repository:%s:pull", img)
req, err := http.NewRequest("GET", authTokenURL, nil)
if err != nil {
return "", err
}
client := &http.Client{Timeout: 10 * time.Second}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", nil
}
tokenRes := struct {
Token string
Expiry string
Issued string
}{}
if err := json.Unmarshal(body, &tokenRes); err != nil {
return "", err
}
return tokenRes.Token, nil
}
// GetTagDigest retrieves an image digest from a v2 registry
func GetTagDigest(image reference.Named) (string, error) {
img := reference.Path(image.(reference.Named))
tag := image.(reference.NamedTagged).Tag()
manifestURL := fmt.Sprintf("https://index.docker.io/v2/%s/manifests/%s", img, tag)
req, err := http.NewRequest("GET", manifestURL, nil)
if err != nil {
return "", err
}
token, err := getRegv2Token(image)
if err != nil {
return "", err
}
req.Header = http.Header{
"Accept": []string{
"application/vnd.docker.distribution.manifest.v2+json",
"application/vnd.docker.distribution.manifest.list.v2+json",
},
"Authorization": []string{fmt.Sprintf("Bearer %s", token)},
}
client := &http.Client{Timeout: 10 * time.Second}
res, err := client.Do(req)
if err != nil {
return "", err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
_, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
registryRes := struct {
SchemaVersion int
MediaType string
Manifests []struct {
MediaType string
Size int
Digest string
Platform struct {
Architecture string
Os string
}
}
}{}
if err := json.Unmarshal(body, &registryRes); err != nil {
return "", err
}
var digest string
for _, manifest := range registryRes.Manifests {
if string(manifest.Platform.Architecture) == "amd64" {
digest = strings.Split(manifest.Digest, ":")[1][:7]
}
}
if digest == "" {
return "", fmt.Errorf("Unable to retrieve amd64 digest for '%s'", image)
}
return digest, nil
}

View File

@ -296,3 +296,49 @@ func UpdateAppComposeTag(recipe, image, tag string) error {
return nil
}
func UpdateAppComposeLabel(recipe, serviceName, newLabel string) error {
pattern := fmt.Sprintf("%s/%s/compose**yml", APPS_DIR, recipe)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
return err
}
for _, composeFile := range composeFiles {
opts := options.Deploy{Composefiles: []string{composeFile}}
compose, err := loader.LoadComposefile(opts)
if err != nil {
return err
}
serviceExists := false
var service composetypes.ServiceConfig
for _, s := range compose.Services {
if s.Name == serviceName {
service = s
serviceExists = true
}
}
if !serviceExists {
continue
}
for oldLabel, value := range service.Deploy.Labels {
if strings.HasPrefix(oldLabel, "coop-cloud") {
bytes, err := ioutil.ReadFile(composeFile)
if err != nil {
return err
}
old := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s", service.Name, value)
replacedBytes := strings.Replace(string(bytes), old, newLabel, -1)
if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil {
return err
}
}
}
}
return nil
}