Skip to content

Commit

Permalink
enable releasing and deploying OCI artefacts
Browse files Browse the repository at this point in the history
  • Loading branch information
joonas-fi committed Aug 15, 2024
1 parent 31026a7 commit a7bbcef
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 8 deletions.
4 changes: 3 additions & 1 deletion cmd/deployer/artefactdownloaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,16 @@ func (f *localFileDownloader) DownloadArtefact(ctx context.Context, filename str
return file, nil
}

func makeArtefactDownloader(uri string, gmc *githubminiclient.Client) (artefactDownloader, error) {
func makeArtefactDownloader(ctx context.Context, uri string, gmc *githubminiclient.Client) (artefactDownloader, error) {
switch {
case strings.HasPrefix(uri, "file:"):
return newLocalFileDownloader(uri[len("file:"):]), nil
case strings.HasPrefix(uri, "http:"), strings.HasPrefix(uri, "https:"):
return newhttpArtefactDownloader(uri), nil
case strings.HasPrefix(uri, "githubrelease:"):
return newGithubReleasesArtefactDownloader(uri, gmc)
case strings.HasPrefix(uri, "docker://"):
return newOCIArtefactDownloader(ctx, uri[len("docker://"):])
default:
return nil, fmt.Errorf("unsupported URI: %s", uri)
}
Expand Down
9 changes: 5 additions & 4 deletions cmd/deployer/artefactdownloaders_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package main

import (
"context"
"testing"

"github.com/function61/gokit/assert"
)

func TestGithubReleases(t *testing.T) {
downloader, err := makeArtefactDownloader("githubrelease:function61:coolproduct:12345", nil)
downloader, err := makeArtefactDownloader(context.TODO(), "githubrelease:function61:coolproduct:12345", nil)

assert.Ok(t, err)

Expand All @@ -19,7 +20,7 @@ func TestGithubReleases(t *testing.T) {
}

func TestHttp(t *testing.T) {
downloader, err := makeArtefactDownloader("http://downloads.example.com/", nil)
downloader, err := makeArtefactDownloader(context.TODO(), "http://downloads.example.com/", nil)

assert.Ok(t, err)

Expand All @@ -29,7 +30,7 @@ func TestHttp(t *testing.T) {
}

func TestHttps(t *testing.T) {
downloader, err := makeArtefactDownloader("https://downloads.example.com/", nil)
downloader, err := makeArtefactDownloader(context.TODO(), "https://downloads.example.com/", nil)

assert.Ok(t, err)

Expand All @@ -39,7 +40,7 @@ func TestHttps(t *testing.T) {
}

func TestUnsupportedUri(t *testing.T) {
_, err := makeArtefactDownloader("ftp://stuff", nil)
_, err := makeArtefactDownloader(context.TODO(), "ftp://stuff", nil)

assert.EqualString(t, err.Error(), "unsupported URI: ftp://stuff")
}
85 changes: 85 additions & 0 deletions cmd/deployer/ociartefactdownloader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package main

import (
"bytes"
"context"
"fmt"
"io"
"os/exec"
"strings"

"github.com/function61/deployer/pkg/oci"
"github.com/function61/gokit/jsonfile"
)

type ociArtefactDownloader struct {
imageRef string
imageRefWithoutTag string
manifest oci.Manifest
}

func newOCIArtefactDownloader(ctx context.Context, imageRef string) (artefactDownloader, error) {
withErr := func(err error) (artefactDownloader, error) {
return nil, fmt.Errorf("newOCIArtefactDownloader: %w", err)
}

orasFetchOutput := &bytes.Buffer{}
stderr := &bytes.Buffer{}

orasFetch := exec.CommandContext(ctx, "oras", "manifest", "fetch", imageRef)
orasFetch.Stdout = orasFetchOutput
orasFetch.Stderr = stderr

if err := orasFetch.Run(); err != nil {
return withErr(fmt.Errorf("oras manifest fetch %s: %w: stdout[%s] stderr[%s]", imageRef, err, orasFetchOutput.String(), stderr.String()))
}

manifest := oci.Manifest{}
if err := jsonfile.Unmarshal(orasFetchOutput, &manifest, false); err != nil {
return withErr(err)
}

// "redis:latest" => ["redis", "latest"]
imageRefWithoutTag := strings.Split(imageRef, ":")
if len(imageRefWithoutTag) != 2 {
return withErr(fmt.Errorf("expected imageRef to be <something>:<tag>; got %s", imageRef))
}

return &ociArtefactDownloader{imageRef, imageRefWithoutTag[0], manifest}, nil
}

func (o *ociArtefactDownloader) DownloadArtefact(ctx context.Context, filename string) (io.ReadCloser, error) {
withErr := func(err error) (io.ReadCloser, error) {
return nil, fmt.Errorf("ociArtefactDownloader.DownloadArtefact: %w", err)
}

layer, found := func() (*oci.Layer, bool) {
for _, layer := range o.manifest.Layers {
if layer.Annotations["org.opencontainers.image.title"] == filename {
return &layer, true
}
}

return nil, false
}()
if !found {
return withErr(fmt.Errorf("%s not found from manifest of %s", filename, o.imageRef))
}

// "redis" => "redis@sha256:..."
blobRef := o.imageRefWithoutTag + "@" + layer.Digest

// blobReader, blobWriter := io.Pipe()

blobReader := &bytes.Buffer{}

orasBlobFetch := exec.CommandContext(ctx, "oras", "blob", "fetch", "--output=-", blobRef)
orasBlobFetch.Stdout = blobReader
// orasBlobFetch.Stdout = blobWriter

if err := orasBlobFetch.Run(); err != nil {
return withErr(err)
}

return io.NopCloser(blobReader), nil
}
52 changes: 52 additions & 0 deletions cmd/deployer/ociimagerelease.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

// OCI image releaser

import (
"context"
"log"
"time"

"github.com/function61/deployer/pkg/ddomain"
"github.com/function61/deployer/pkg/dstate"
"github.com/function61/deployer/pkg/githubminiclient"
"github.com/function61/eventhorizon/pkg/ehevent"
"github.com/function61/gokit/cryptorandombytes"
)

func createOCIImageRelease(
ctx context.Context,
imageRef string,
owner string,
repoName string,
releaseName string,
revisionId string,
logger *log.Logger,
) error {
repo := githubminiclient.NewRepoRef(owner, repoName)

app, err := mkApp(ctx)
if err != nil {
return err
}

if app.State.HasRevisionId(revisionId) { // should not be considered an error
logger.Printf("WARN: already have revision %s", revisionId)
return nil
}

releaseCreated := ddomain.NewReleaseCreated(
cryptorandombytes.Base64UrlWithoutLeadingDash(4),
ownerSlashRepo(repo), // function61/coolproduct
releaseName,
revisionId,
"docker://"+imageRef,
"",
ehevent.MetaSystemUser(time.Now())) // TODO: time of commit?

_, err = app.Writer.Append(
ctx,
app.TenantCtx.Tenant.Stream(dstate.Stream),
[]string{ehevent.Serialize(releaseCreated)})
return err
}
19 changes: 18 additions & 1 deletion cmd/deployer/releases.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ func downloadReleaseWith(

log.Printf("artefacts source: %s", artefactsLocation)

artefacts, err := makeArtefactDownloader(artefactsLocation, gmc)
artefacts, err := makeArtefactDownloader(ctx, artefactsLocation, gmc)
if err != nil {
return err
}
Expand Down Expand Up @@ -230,6 +230,23 @@ func releasesEntry(logger *log.Logger) *cobra.Command {

cmd.AddCommand(listReleasesEntrypoint(logger))

cmd.AddCommand(&cobra.Command{
Use: "oci-image-release-mk [imageRef] [owner] [repo] [releaseName] [revisionId]",
Short: "Create (container) image release",
Args: cobra.ExactArgs(5),
Run: func(_ *cobra.Command, args []string) {
exitWithErrorIfErr(createOCIImageRelease(
ossignal.InterruptOrTerminateBackgroundCtx(logger),
args[0],
args[1],
args[2],
args[3],
args[4],
logger,
))
},
})

cmd.AddCommand(&cobra.Command{
Use: "githubrelease-mk [owner] [repo] [releaseName] [revisionId] [assetDir]",
Short: "Create GitHub release",
Expand Down
4 changes: 2 additions & 2 deletions pkg/ddomain/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ type ReleaseCreated struct {
Repository string
RevisionFriendly string
RevisionId string
ArtefactsLocation string // "https://baseurl" if easy to download. or "githubrelease:owner:reponame:releaseId"
DeployerSpecFilename string // usually "deployerspec.zip"
ArtefactsLocation string // "https://baseurl" if easy to download. "githubrelease:owner:reponame:releaseId" for GitHub releases. "docker://<image ref>" for (container) images.
DeployerSpecFilename string `json:",omitempty"` // usually "deployerspec.zip"
}

func (e *ReleaseCreated) MetaType() string { return "ReleaseCreated" }
Expand Down
24 changes: 24 additions & 0 deletions pkg/oci/oci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package oci

type Manifest struct {
SchemaVersion int `json:"schemaVersion"`
MediaType string `json:"mediaType"`
ArtifactType string `json:"artifactType"`
Config Config `json:"config"`
Layers []Layer `json:"layers"`
Annotations map[string]string `json:"annotations"`
}

type Config struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int `json:"size"`
Data string `json:"data"`
}

type Layer struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int `json:"size"`
Annotations map[string]string `json:"annotations"`
}

0 comments on commit a7bbcef

Please sign in to comment.