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

Add json/yaml output to flux push artifact #3540

Merged
merged 2 commits into from
Jan 31, 2023
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
46 changes: 27 additions & 19 deletions action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,25 @@ jobs:
flux tag artifact $OCI_REPO:$(git rev-parse --short HEAD) --tag staging
```

Example workflow for publishing Kubernetes manifests bundled as OCI artifacts to Docker Hub:
### Push and sign Kubernetes manifests to container registries

Example workflow for publishing Kubernetes manifests bundled as OCI artifacts
which are signed with Cosign and GitHub OIDC:

```yaml
name: push-artifact-production
name: push-sign-artifact

on:
push:
tags:
- '*'
branches:
- 'main'

permissions:
packages: write # needed for ghcr.io access
id-token: write # needed for keyless signing

env:
OCI_REPO: "oci://docker.io/my-org/app-config"
OCI_REPO: "oci://ghcr.io/my-org/manifests/${{ github.event.repository.name }}"

jobs:
kubernetes:
Expand All @@ -155,23 +162,24 @@ jobs:
uses: actions/checkout@v3
- name: Setup Flux CLI
uses: fluxcd/flux2/action@main
- name: Login to Docker Hub
- name: Setup Cosign
uses: sigstore/cosign-installer@main
- name: Login to GHCR
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Generate manifests
run: |
kustomize build ./manifests/production > ./deploy/app.yaml
- name: Push manifests
run: |
flux push artifact $OCI_REPO:$(git tag --points-at HEAD) \
--path="./deploy" \
--source="$(git config --get remote.origin.url)" \
--revision="$(git tag --points-at HEAD)/$(git rev-parse HEAD)"
- name: Deploy manifests to production
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push and sign manifests
run: |
flux tag artifact $OCI_REPO:$(git tag --points-at HEAD) --tag production
digest_url=$(flux push artifact \
$OCI_REPO:$(git rev-parse --short HEAD) \
--path="./manifests" \
--source="$(git config --get remote.origin.url)" \
--revision="$(git branch --show-current)/$(git rev-parse HEAD)" |\
jq -r '. | .repository + "@" + .digest')

cosign sign $digest_url
```

### End-to-end testing
Expand Down
62 changes: 59 additions & 3 deletions cmd/flux/push_artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ package main

import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/fluxcd/flux2/internal/flags"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
reg "github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"

oci "github.com/fluxcd/pkg/oci/client"
)
Expand All @@ -40,6 +43,16 @@ The command can read the credentials from '~/.docker/config.json' but they can a
--source="$(git config --get remote.origin.url)" \
--revision="$(git branch --show-current)/$(git rev-parse HEAD)"

# Push and sign artifact with cosign
digest_url = $(flux push artifact \
oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) \
--source="$(git config --get remote.origin.url)" \
--revision="$(git branch --show-current)/$(git rev-parse HEAD)" \
--path="./path/to/local/manifest.yaml" \
--output json | \
jq -r '. | .repository + "@" + .digest')
cosign sign $digest_url

# Push manifests passed into stdin to GHCR
kustomize build . | flux push artifact oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) -p - \
--source="$(git config --get remote.origin.url)" \
Expand Down Expand Up @@ -85,6 +98,7 @@ type pushArtifactFlags struct {
creds string
provider flags.SourceOCIProvider
ignorePaths []string
output string
}

var pushArtifactArgs = newPushArtifactFlags()
Expand All @@ -102,6 +116,8 @@ func init() {
pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format <username>[:<password>] if --provider is generic")
pushArtifactCmd.Flags().Var(&pushArtifactArgs.provider, "provider", pushArtifactArgs.provider.Description())
pushArtifactCmd.Flags().StringSliceVar(&pushArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
pushArtifactCmd.Flags().StringVarP(&pushArtifactArgs.output, "output", "o", "",
"the format in which the artifact digest should be printed, can be 'json' or 'yaml'")

pushCmd.AddCommand(pushArtifactCmd)
}
Expand Down Expand Up @@ -172,14 +188,54 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error {
}
}

logger.Actionf("pushing artifact to %s", url)
if pushArtifactArgs.output == "" {
logger.Actionf("pushing artifact to %s", url)
}

digest, err := ociClient.Push(ctx, url, path, meta, pushArtifactArgs.ignorePaths)
digestURL, err := ociClient.Push(ctx, url, path, meta, pushArtifactArgs.ignorePaths)
if err != nil {
return fmt.Errorf("pushing artifact failed: %w", err)
}

logger.Successf("artifact successfully pushed to %s", digest)
digest, err := reg.NewDigest(digestURL)
if err != nil {
return fmt.Errorf("artifact digest parsing failed: %w", err)
}

tag, err := reg.NewTag(url)
if err != nil {
return fmt.Errorf("artifact tag parsing failed: %w", err)
}

info := struct {
URL string `json:"url"`
Repository string `json:"repository"`
Tag string `json:"tag"`
Digest string `json:"digest"`
}{
URL: fmt.Sprintf("oci://%s", digestURL),
Repository: digest.Repository.Name(),
Tag: tag.TagStr(),
Digest: digest.DigestStr(),
}

switch pushArtifactArgs.output {
case "json":
marshalled, err := json.MarshalIndent(&info, "", " ")
if err != nil {
return fmt.Errorf("artifact digest JSON conversion failed: %w", err)
}
marshalled = append(marshalled, "\n"...)
rootCmd.Print(string(marshalled))
case "yaml":
marshalled, err := yaml.Marshal(&info)
if err != nil {
return fmt.Errorf("artifact digest YAML conversion failed: %w", err)
}
rootCmd.Print(string(marshalled))
default:
logger.Successf("artifact successfully pushed to %s", digestURL)
}

return nil
}