Use fluxcd/pkg/oci/client
Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
@@ -1,139 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Build archives the given directory as a tarball to the given local path.
|
||||
// While archiving, any environment specific data (for example, the user and group name) is stripped from file headers.
|
||||
func Build(artifactPath, sourceDir string) (err error) {
|
||||
if f, err := os.Stat(sourceDir); os.IsNotExist(err) || !f.IsDir() {
|
||||
return fmt.Errorf("invalid source dir path: %s", sourceDir)
|
||||
}
|
||||
|
||||
tf, err := os.CreateTemp(filepath.Split(sourceDir))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpName := tf.Name()
|
||||
defer func() {
|
||||
if err != nil {
|
||||
os.Remove(tmpName)
|
||||
}
|
||||
}()
|
||||
|
||||
sz := &writeCounter{}
|
||||
mw := io.MultiWriter(tf, sz)
|
||||
|
||||
gw := gzip.NewWriter(mw)
|
||||
tw := tar.NewWriter(gw)
|
||||
if err := filepath.Walk(sourceDir, func(p string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ignore anything that is not a file or directories e.g. symlinks
|
||||
if m := fi.Mode(); !(m.IsRegular() || m.IsDir()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
header, err := tar.FileInfoHeader(fi, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The name needs to be modified to maintain directory structure
|
||||
// as tar.FileInfoHeader only has access to the base name of the file.
|
||||
// Ref: https://golang.org/src/archive/tar/common.go?#L626
|
||||
relFilePath := p
|
||||
if filepath.IsAbs(sourceDir) {
|
||||
relFilePath, err = filepath.Rel(sourceDir, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
header.Name = relFilePath
|
||||
|
||||
// Remove any environment specific data.
|
||||
header.Gid = 0
|
||||
header.Uid = 0
|
||||
header.Uname = ""
|
||||
header.Gname = ""
|
||||
header.ModTime = time.Time{}
|
||||
header.AccessTime = time.Time{}
|
||||
header.ChangeTime = time.Time{}
|
||||
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !fi.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(tw, f); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}); err != nil {
|
||||
tw.Close()
|
||||
gw.Close()
|
||||
tf.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tw.Close(); err != nil {
|
||||
gw.Close()
|
||||
tf.Close()
|
||||
return err
|
||||
}
|
||||
if err := gw.Close(); err != nil {
|
||||
tf.Close()
|
||||
return err
|
||||
}
|
||||
if err := tf.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chmod(tmpName, 0o640); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.Rename(tmpName, artifactPath)
|
||||
}
|
||||
|
||||
type writeCounter struct {
|
||||
written int64
|
||||
}
|
||||
|
||||
func (wc *writeCounter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
wc.written += int64(n)
|
||||
return n, nil
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
testDir := "./testdata/build"
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
artifactPath := filepath.Join(tmpDir, "files.tar.gz")
|
||||
|
||||
// test with non-existent path
|
||||
err := Build(artifactPath, "testdata/non-existent")
|
||||
g.Expect(err).To(HaveOccurred())
|
||||
|
||||
err = Build(artifactPath, testDir)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
if _, err := os.Stat(artifactPath); err != nil {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
// List fetches the tags and their manifests for a given OCI repository.
|
||||
func List(ctx context.Context, url string) ([]Metadata, error) {
|
||||
metas := make([]Metadata, 0)
|
||||
tags, err := crane.ListTags(url, craneOptions(ctx)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing tags failed: %w", err)
|
||||
}
|
||||
|
||||
sort.Slice(tags, func(i, j int) bool { return tags[i] > tags[j] })
|
||||
|
||||
for _, tag := range tags {
|
||||
// exclude cosign signatures
|
||||
if strings.HasSuffix(tag, ".sig") {
|
||||
continue
|
||||
}
|
||||
|
||||
meta := Metadata{
|
||||
URL: fmt.Sprintf("%s:%s", url, tag),
|
||||
}
|
||||
|
||||
manifestJSON, err := crane.Manifest(meta.URL, craneOptions(ctx)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching manifest failed: %w", err)
|
||||
}
|
||||
|
||||
manifest, err := gcrv1.ParseManifest(bytes.NewReader(manifestJSON))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing manifest failed: %w", err)
|
||||
}
|
||||
|
||||
if m, err := MetadataFromAnnotations(manifest.Annotations); err == nil {
|
||||
meta.Revision = m.Revision
|
||||
meta.Source = m.Source
|
||||
}
|
||||
|
||||
digest, err := crane.Digest(meta.URL, craneOptions(ctx)...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching digest failed: %w", err)
|
||||
}
|
||||
meta.Digest = digest
|
||||
|
||||
metas = append(metas, meta)
|
||||
}
|
||||
|
||||
return metas, nil
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/random"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func Test_List(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
ctx := context.Background()
|
||||
repo := "test-list" + randStringRunes(5)
|
||||
tags := []string{"v0.0.1", "v0.0.2", "v0.0.3"}
|
||||
source := "github.com/fluxcd/fluxv2"
|
||||
rev := "rev"
|
||||
m := Metadata{
|
||||
Source: source,
|
||||
Revision: rev,
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
dst := fmt.Sprintf("%s/%s:%s", dockerReg, repo, tag)
|
||||
img, err := random.Image(1024, 1)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
img = mutate.Annotations(img, m.ToAnnotations()).(gcrv1.Image)
|
||||
err = crane.Push(img, dst, craneOptions(ctx)...)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
metadata, err := List(ctx, fmt.Sprintf("%s/%s", dockerReg, repo))
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
g.Expect(len(metadata)).To(Equal(len(tags)))
|
||||
for _, meta := range metadata {
|
||||
tag, err := name.NewTag(meta.URL)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(tag.TagStr()).Should(BeElementOf(tags))
|
||||
|
||||
g.Expect(meta.ToAnnotations()).To(Equal(m.ToAnnotations()))
|
||||
|
||||
digest, err := crane.Digest(meta.URL, craneOptions(ctx)...)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(meta.Digest).To(Equal(digest))
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
SourceAnnotation = "org.opencontainers.image.source"
|
||||
RevisionAnnotation = "org.opencontainers.image.revision"
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
Source string `json:"source_url"`
|
||||
Revision string `json:"source_revision"`
|
||||
Digest string `json:"digest"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func (m *Metadata) ToAnnotations() map[string]string {
|
||||
annotations := map[string]string{
|
||||
SourceAnnotation: m.Source,
|
||||
RevisionAnnotation: m.Revision,
|
||||
}
|
||||
|
||||
return annotations
|
||||
}
|
||||
|
||||
func MetadataFromAnnotations(annotations map[string]string) (*Metadata, error) {
|
||||
source, ok := annotations[SourceAnnotation]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' annotation not found", SourceAnnotation)
|
||||
}
|
||||
|
||||
revision, ok := annotations[RevisionAnnotation]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' annotation not found", RevisionAnnotation)
|
||||
}
|
||||
|
||||
m := Metadata{
|
||||
Source: source,
|
||||
Revision: revision,
|
||||
}
|
||||
|
||||
return &m, nil
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fluxcd/pkg/untar"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
// Pull downloads an artifact from an OCI repository and extracts the content to the given directory.
|
||||
func Pull(ctx context.Context, url, outDir string) (*Metadata, error) {
|
||||
ref, err := name.ParseReference(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
|
||||
img, err := crane.Pull(url, craneOptions(ctx)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
digest, err := img.Digest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing digest failed: %w", err)
|
||||
}
|
||||
|
||||
manifest, err := img.Manifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing manifest failed: %w", err)
|
||||
}
|
||||
|
||||
meta, err := MetadataFromAnnotations(manifest.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta.Digest = ref.Context().Digest(digest.String()).String()
|
||||
|
||||
layers, err := img.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list layers: %w", err)
|
||||
}
|
||||
|
||||
if len(layers) < 1 {
|
||||
return nil, fmt.Errorf("no layers found in artifact")
|
||||
}
|
||||
|
||||
blob, err := layers[0].Compressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extracting first layer failed: %w", err)
|
||||
}
|
||||
|
||||
if _, err = untar.Untar(blob, outDir); err != nil {
|
||||
return nil, fmt.Errorf("failed to untar first layer: %w", err)
|
||||
}
|
||||
|
||||
return meta, nil
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
)
|
||||
|
||||
// Push creates an artifact from the given directory, uploads the artifact
|
||||
// to the given OCI repository and returns the digest.
|
||||
func Push(ctx context.Context, url, sourceDir string, meta Metadata) (string, error) {
|
||||
ref, err := name.ParseReference(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "oci")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
tmpFile := filepath.Join(tmpDir, "artifact.tgz")
|
||||
|
||||
if err := Build(tmpFile, sourceDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
img, err := crane.Append(empty.Image, tmpFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("appeding content to artifact failed: %w", err)
|
||||
}
|
||||
|
||||
img = mutate.Annotations(img, meta.ToAnnotations()).(gcrv1.Image)
|
||||
|
||||
if err := crane.Push(img, url, craneOptions(ctx)...); err != nil {
|
||||
return "", fmt.Errorf("pushing artifact failed: %w", err)
|
||||
}
|
||||
|
||||
digest, err := img.Digest()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing artifact digest failed: %w", err)
|
||||
}
|
||||
|
||||
return ref.Context().Digest(digest.String()).String(), err
|
||||
}
|
||||
|
||||
func craneOptions(ctx context.Context) []crane.Option {
|
||||
return []crane.Option{
|
||||
crane.WithContext(ctx),
|
||||
crane.WithUserAgent("flux/v2"),
|
||||
crane.WithPlatform(&gcrv1.Platform{
|
||||
Architecture: "flux",
|
||||
OS: "flux",
|
||||
OSVersion: "v2",
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func Test_Push_Pull(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
ctx := context.Background()
|
||||
tag := "v0.0.1"
|
||||
repo := "test-push" + randStringRunes(5)
|
||||
|
||||
url := fmt.Sprintf("%s/%s:%s", dockerReg, repo, tag)
|
||||
metadata := Metadata{
|
||||
Source: "github.com/fluxcd/flux2",
|
||||
Revision: "rev",
|
||||
}
|
||||
annotations := metadata.ToAnnotations()
|
||||
|
||||
testDir := "testdata/build"
|
||||
_, err := Push(ctx, url, testDir, metadata)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
tags, err := crane.ListTags(fmt.Sprintf("%s/%s", dockerReg, repo))
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(len(tags)).To(BeEquivalentTo(1))
|
||||
|
||||
image, err := crane.Pull(fmt.Sprintf("%s/%s:%s", dockerReg, repo, tag))
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
manifest, err := image.Manifest()
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(manifest.Annotations).To(BeEquivalentTo(annotations))
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
_, err = Pull(ctx, url, tmpDir)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
// Walk directory the test directory and check that all paths exists in the extracted archive
|
||||
err = filepath.Walk(testDir, func(path string, info fs.FileInfo, err error) error {
|
||||
tmpPath := filepath.Join(tmpDir, path)
|
||||
if _, err := os.Stat(tmpPath); err != nil && os.IsNotExist(err) {
|
||||
return fmt.Errorf("path '%s' doesn't exist in archive", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
"github.com/distribution/distribution/v3/registry"
|
||||
_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
|
||||
"github.com/phayes/freeport"
|
||||
)
|
||||
|
||||
var (
|
||||
dockerReg string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func setupRegistryServer(ctx context.Context) error {
|
||||
// Registry config
|
||||
config := &configuration.Configuration{}
|
||||
port, err := freeport.GetFreePort()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get free port: %s", err)
|
||||
}
|
||||
|
||||
dockerReg = fmt.Sprintf("localhost:%d", port)
|
||||
config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port)
|
||||
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
|
||||
config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
|
||||
dockerRegistry, err := registry.NewRegistry(ctx, config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker registry: %w", err)
|
||||
}
|
||||
|
||||
// Start Docker registry
|
||||
go dockerRegistry.ListenAndServe()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
ctx := ctrl.SetupSignalHandler()
|
||||
err := setupRegistryServer(ctx)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to start docker registry: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
||||
|
||||
func randStringRunes(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
// Tag creates a new tag for the given artifact using the same OCI repository as the origin.
|
||||
func Tag(ctx context.Context, url, tag string) (string, error) {
|
||||
ref, err := name.ParseReference(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
|
||||
if err := crane.Tag(url, tag, craneOptions(ctx)...); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dst := ref.Context().Tag(tag)
|
||||
|
||||
return dst.Name(), nil
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
"github.com/google/go-containerregistry/pkg/v1/random"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func Test_Tag(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
ctx := context.Background()
|
||||
testRepo := "test-tag"
|
||||
url := fmt.Sprintf("%s/%s:v0.0.1", dockerReg, testRepo)
|
||||
img, err := random.Image(1024, 1)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
err = crane.Push(img, url, craneOptions(ctx)...)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
_, err = Tag(ctx, url, "v0.0.2")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
tags, err := crane.ListTags(fmt.Sprintf("%s/%s", dockerReg, testRepo))
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
g.Expect(len(tags)).To(BeEquivalentTo(2))
|
||||
g.Expect(tags).To(BeEquivalentTo([]string{"v0.0.1", "v0.0.2"}))
|
||||
}
|
||||
21
internal/oci/testdata/build/deployment.yaml
vendored
21
internal/oci/testdata/build/deployment.yaml
vendored
@@ -1,21 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:1.14.2
|
||||
ports:
|
||||
- containerPort: 80
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: podinfo
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 2m
|
||||
url: https://stefanprodan.github.io/podinfo
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
Copyright 2022 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
)
|
||||
|
||||
// ParseArtifactURL validates the OCI URL and returns the address of the artifact.
|
||||
func ParseArtifactURL(ociURL string) (string, error) {
|
||||
if !strings.HasPrefix(ociURL, sourcev1.OCIRepositoryPrefix) {
|
||||
return "", fmt.Errorf("URL must be in format 'oci://<domain>/<org>/<repo>'")
|
||||
}
|
||||
|
||||
url := strings.TrimPrefix(ociURL, sourcev1.OCIRepositoryPrefix)
|
||||
if _, err := name.ParseReference(url); err != nil {
|
||||
return "", fmt.Errorf("'%s' invalid URL: %w", ociURL, err)
|
||||
}
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// ParseRepositoryURL validates the OCI URL and returns the address of the artifact repository.
|
||||
func ParseRepositoryURL(ociURL string) (string, error) {
|
||||
if !strings.HasPrefix(ociURL, sourcev1.OCIRepositoryPrefix) {
|
||||
return "", fmt.Errorf("URL must be in format 'oci://<domain>/<org>/<repo>'")
|
||||
}
|
||||
|
||||
url := strings.TrimPrefix(ociURL, sourcev1.OCIRepositoryPrefix)
|
||||
ref, err := name.ParseReference(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("'%s' invalid URL: %w", ociURL, err)
|
||||
}
|
||||
|
||||
return ref.Context().Name(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user