Skip to content
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
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.24 AS build
ARG TARGETOS=linux
ARG TARGETARCH=amd64

WORKDIR /concourse-oci-helm-chart-resource
COPY . .
RUN make build
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} make build

FROM --platform=${BUILDPLATFORM:-linux/amd64} alpine:3.22.0 AS run
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine:3.22.0 AS run

# upgrade all installed packages to fix potential CVEs in advance
RUN apk upgrade --no-cache --no-progress \
Expand Down
15 changes: 13 additions & 2 deletions cmd/out/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,31 @@ func main() {
decoder := json.NewDecoder(os.Stdin)
if err := decoder.Decode(&req); err != nil {
fmt.Fprintf(os.Stderr, "failed to unmarshal request: %s\n", err)
os.Exit(1)
}

if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "missing arguments")
os.Exit(1)
}
if err := req.Validate(); err != nil {
fmt.Fprintf(os.Stderr, "invalid source configuration: %s\n", err)
os.Exit(1)
}
inputDir := os.Args[1]
response, err := resource.Put(context.Background(), req, inputDir)
ctx := context.Background()
repo, err := resource.NewRepositoryForSource(ctx, req.Source)
if err != nil {
fmt.Fprintf(os.Stderr, "get failed: %s\n", err)
fmt.Fprintf(os.Stderr, "failed to create repository: %s\n", err)
os.Exit(1)
}
response, err := resource.Put(ctx, req, inputDir, repo)
if err != nil {
fmt.Fprintf(os.Stderr, "put failed: %s\n", err)
os.Exit(1)
}
if err := json.NewEncoder(os.Stdout).Encode(response); err != nil {
fmt.Fprintf(os.Stderr, "failed to marshal response: %s\n", err)
os.Exit(1)
}
}
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ go 1.23.0

require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/errors v0.9.1
oras.land/oras-go/v2 v2.6.0
)

require (
github.com/opencontainers/go-digest v1.0.0 // indirect
golang.org/x/sync v0.14.0 // indirect
)
require golang.org/x/sync v0.14.0 // indirect
3 changes: 2 additions & 1 deletion pkg/resource/in.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,13 @@ func Get(ctx context.Context, request GetRequest, outputDir string, repo Reposit
}

// Find different layers.
configMediaType := request.Source.GetConfigMediaType()
for _, layer := range manifestDescriptor.Layers {
var fileExtension string
switch layer.MediaType {
case mediaTypeHelmChartContentArchive:
fileExtension = ".tgz"
case mediaTypeHelmChartJSON:
case mediaTypeHelmChartJSON, configMediaType:
fileExtension = ".json"
default:
continue
Expand Down
122 changes: 116 additions & 6 deletions pkg/resource/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,129 @@
package resource

import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"

digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
oras "oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/memory"
)

type (
PutRequest struct{}
PutResponse struct{}
PutRequest struct {
Source Source `json:"source"`
Params PutParams `json:"params"`
}

PutParams struct {
ChartDir string `json:"chart_dir"`
}

PutResponse struct {
Version Version `json:"version"`
Metadata []MetadataItem `json:"metadata,omitempty"`
}
)

func (pr *PutRequest) Validate() error {
return nil
if pr.Params.ChartDir == "" {
return errors.New("params.chart_dir is required")
}
return pr.Source.Validate()
}

func Put(ctx context.Context, request PutRequest, inputDir string) (*PutResponse, error) {
return nil, errors.New("not implemented")
func Put(ctx context.Context, request PutRequest, inputDir string, target oras.Target) (*PutResponse, error) {
chartDir := filepath.Join(inputDir, request.Params.ChartDir)
matches, err := filepath.Glob(filepath.Join(chartDir, "*.tgz"))
if err != nil {
return nil, errors.Wrap(err, "failed to glob for chart packages")
}
if len(matches) == 0 {
return nil, fmt.Errorf("no .tgz files found in %s", chartDir)
}
if len(matches) > 1 {
return nil, fmt.Errorf("multiple .tgz files found in %s, expected exactly one", chartDir)
}

chartPath := matches[0]
chartFilename := filepath.Base(chartPath)

// Extract version tag from filename: <chart-name>-<version>.tgz
prefix := request.Source.ChartName + "-"
if !strings.HasPrefix(chartFilename, prefix) {
return nil, fmt.Errorf("chart filename %q does not start with expected prefix %q", chartFilename, prefix)
}
tag := strings.TrimSuffix(strings.TrimPrefix(chartFilename, prefix), ".tgz")
if tag == "" {
return nil, fmt.Errorf("could not extract version tag from filename %q", chartFilename)
}

chartContent, err := os.ReadFile(chartPath)
if err != nil {
return nil, errors.Wrap(err, "failed to read chart file")
}

fmt.Fprintf(os.Stderr, "pushing %s version %s to %s\n", request.Source.ChartName, tag, request.Source.String())

store := memory.New()

// Push chart layer
chartDesc := ocispec.Descriptor{
MediaType: mediaTypeHelmChartContentArchive,
Digest: digest.FromBytes(chartContent),
Size: int64(len(chartContent)),
}
if err := store.Push(ctx, chartDesc, bytes.NewReader(chartContent)); err != nil {
return nil, errors.Wrap(err, "failed to push chart layer to store")
}

// Push empty helm chart config
configContent := []byte("{}")
configDesc := ocispec.Descriptor{
MediaType: request.Source.GetConfigMediaType(),
Digest: digest.FromBytes(configContent),
Size: int64(len(configContent)),
}
if err := store.Push(ctx, configDesc, bytes.NewReader(configContent)); err != nil {
return nil, errors.Wrap(err, "failed to push config to store")
}

// Pack OCI manifest
packOpts := oras.PackManifestOptions{
Layers: []ocispec.Descriptor{chartDesc},
ConfigDescriptor: &configDesc,
}
manifestDesc, err := oras.PackManifest(ctx, store, oras.PackManifestVersion1_1, "", packOpts)
if err != nil {
return nil, errors.Wrap(err, "failed to pack manifest")
}

if err := store.Tag(ctx, manifestDesc, tag); err != nil {
return nil, errors.Wrap(err, "failed to tag manifest")
}

// Push to remote registry
desc, err := oras.Copy(ctx, store, tag, target, tag, oras.DefaultCopyOptions)
if err != nil {
return nil, errors.Wrapf(err, "failed to push chart %s:%s", request.Source.String(), tag)
}

fmt.Fprintf(os.Stderr, "pushed %s:%s (digest: %s)\n", request.Source.String(), tag, desc.Digest.String())

return &PutResponse{
Version: Version{
Tag: tag,
Digest: desc.Digest.String(),
},
Metadata: []MetadataItem{
{Name: "chart", Value: request.Source.ChartName},
{Name: "version", Value: tag},
},
}, nil
}
Loading
Loading