mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
1df743f173
fix(gc): fix cleaning deduped blobs because they have the modTime of the original blobs, fixed by updating the modTime when hard linking the blobs. fix(gc): failing to parse rootDir at zot startup when using s3 storage because there are no files under rootDir and we can not create empty dirs on s3, fixed by creating an empty file under rootDir. Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
1610 lines
36 KiB
Go
1610 lines
36 KiB
Go
package test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"math"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/notaryproject/notation-core-go/signature/jws"
|
|
"github.com/notaryproject/notation-core-go/testhelper"
|
|
"github.com/notaryproject/notation-go"
|
|
notconfig "github.com/notaryproject/notation-go/config"
|
|
"github.com/notaryproject/notation-go/dir"
|
|
notreg "github.com/notaryproject/notation-go/registry"
|
|
"github.com/notaryproject/notation-go/signer"
|
|
"github.com/notaryproject/notation-go/verifier"
|
|
godigest "github.com/opencontainers/go-digest"
|
|
"github.com/opencontainers/image-spec/specs-go"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/opencontainers/umoci"
|
|
"github.com/phayes/freeport"
|
|
"github.com/project-zot/mockoidc"
|
|
"github.com/sigstore/cosign/v2/cmd/cosign/cli/generate"
|
|
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
|
|
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gopkg.in/resty.v1"
|
|
"oras.land/oras-go/v2/registry"
|
|
"oras.land/oras-go/v2/registry/remote"
|
|
"oras.land/oras-go/v2/registry/remote/auth"
|
|
|
|
"zotregistry.io/zot/pkg/extensions/monitoring"
|
|
zLog "zotregistry.io/zot/pkg/log"
|
|
"zotregistry.io/zot/pkg/storage"
|
|
"zotregistry.io/zot/pkg/storage/local"
|
|
stypes "zotregistry.io/zot/pkg/storage/types"
|
|
testc "zotregistry.io/zot/pkg/test/common"
|
|
"zotregistry.io/zot/pkg/test/image-utils"
|
|
"zotregistry.io/zot/pkg/test/inject"
|
|
"zotregistry.io/zot/pkg/test/mocks"
|
|
)
|
|
|
|
const (
|
|
BaseURL = "http://127.0.0.1:%s"
|
|
BaseSecureURL = "https://127.0.0.1:%s"
|
|
SleepTime = 100 * time.Millisecond
|
|
)
|
|
|
|
var (
|
|
ErrSignatureVerification = errors.New("signature verification failed")
|
|
ErrAlreadyExists = errors.New("already exists")
|
|
ErrKeyNotFound = errors.New("key not found")
|
|
)
|
|
|
|
var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals
|
|
|
|
// which: manifest, config, layer
|
|
func GetTestBlobDigest(image, which string) godigest.Digest {
|
|
prePath := "../test/data"
|
|
|
|
for _, err := os.Stat(prePath); err != nil; _, err = os.Stat(prePath) {
|
|
prePath = "../" + prePath
|
|
}
|
|
|
|
imgPath := path.Join(prePath, image)
|
|
manifest, config, layer := GetOciLayoutDigests(imgPath)
|
|
|
|
switch which {
|
|
case "manifest":
|
|
return manifest
|
|
case "config":
|
|
return config
|
|
case "layer":
|
|
return layer
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func GetFreePort() string {
|
|
port, err := freeport.GetFreePort()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return fmt.Sprint(port)
|
|
}
|
|
|
|
func GetBaseURL(port string) string {
|
|
return fmt.Sprintf(BaseURL, port)
|
|
}
|
|
|
|
func GetSecureBaseURL(port string) string {
|
|
return fmt.Sprintf(BaseSecureURL, port)
|
|
}
|
|
|
|
func MakeHtpasswdFile() string {
|
|
// bcrypt(username="test", passwd="test")
|
|
content := "test:$2y$05$hlbSXDp6hzDLu6VwACS39ORvVRpr3OMR4RlJ31jtlaOEGnPjKZI1m\n"
|
|
|
|
return MakeHtpasswdFileFromString(content)
|
|
}
|
|
|
|
func GetCredString(username, password string) string {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), 10)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
usernameAndHash := fmt.Sprintf("%s:%s", username, string(hash))
|
|
|
|
return usernameAndHash
|
|
}
|
|
|
|
func MakeHtpasswdFileFromString(fileContent string) string {
|
|
htpasswdFile, err := os.CreateTemp("", "htpasswd-")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// bcrypt(username="test", passwd="test")
|
|
content := []byte(fileContent)
|
|
if err := os.WriteFile(htpasswdFile.Name(), content, 0o600); err != nil { //nolint:gomnd
|
|
panic(err)
|
|
}
|
|
|
|
return htpasswdFile.Name()
|
|
}
|
|
|
|
func CopyFiles(sourceDir, destDir string) error {
|
|
sourceMeta, err := os.Stat(sourceDir)
|
|
if err != nil {
|
|
return fmt.Errorf("CopyFiles os.Stat failed: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil {
|
|
return fmt.Errorf("CopyFiles os.MkdirAll failed: %w", err)
|
|
}
|
|
|
|
files, err := os.ReadDir(sourceDir)
|
|
if err != nil {
|
|
return fmt.Errorf("CopyFiles os.ReadDir failed: %w", err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
sourceFilePath := path.Join(sourceDir, file.Name())
|
|
destFilePath := path.Join(destDir, file.Name())
|
|
|
|
if file.IsDir() {
|
|
if strings.HasPrefix(file.Name(), "_") {
|
|
// Some tests create the trivy related folders under test/_trivy
|
|
continue
|
|
}
|
|
|
|
if err = CopyFiles(sourceFilePath, destFilePath); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
sourceFile, err := os.Open(sourceFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("CopyFiles os.Open failed: %w", err)
|
|
}
|
|
defer sourceFile.Close()
|
|
|
|
destFile, err := os.Create(destFilePath)
|
|
if err != nil {
|
|
return fmt.Errorf("CopyFiles os.Create failed: %w", err)
|
|
}
|
|
defer destFile.Close()
|
|
|
|
if _, err = io.Copy(destFile, sourceFile); err != nil {
|
|
return fmt.Errorf("io.Copy failed: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func CopyTestFiles(sourceDir, destDir string) {
|
|
err := CopyFiles(sourceDir, destDir)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func CopyTestKeysAndCerts(destDir string) error {
|
|
files := []string{
|
|
"ca.crt", "ca.key", "client.cert", "client.csr",
|
|
"client.key", "server.cert", "server.csr", "server.key",
|
|
}
|
|
|
|
rootPath, err := testc.GetProjectRootDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sourceDir := filepath.Join(rootPath, "test/data")
|
|
|
|
sourceMeta, err := os.Stat(sourceDir)
|
|
if err != nil {
|
|
return fmt.Errorf("CopyFiles os.Stat failed: %w", err)
|
|
}
|
|
|
|
if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, file := range files {
|
|
err = CopyFile(filepath.Join(sourceDir, file), filepath.Join(destDir, file))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Controller interface {
|
|
Init(ctx context.Context) error
|
|
Run(ctx context.Context) error
|
|
Shutdown()
|
|
GetPort() int
|
|
}
|
|
|
|
type ControllerManager struct {
|
|
controller Controller
|
|
// used to stop background tasks(goroutines)
|
|
cancelRoutinesFunc context.CancelFunc
|
|
}
|
|
|
|
func (cm *ControllerManager) RunServer(ctx context.Context) {
|
|
// Useful to be able to call in the same goroutine for testing purposes
|
|
if err := cm.controller.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (cm *ControllerManager) StartServer() {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cm.cancelRoutinesFunc = cancel
|
|
|
|
if err := cm.controller.Init(ctx); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
go func() {
|
|
cm.RunServer(ctx)
|
|
}()
|
|
}
|
|
|
|
func (cm *ControllerManager) StopServer() {
|
|
// stop background tasks
|
|
if cm.cancelRoutinesFunc != nil {
|
|
cm.cancelRoutinesFunc()
|
|
}
|
|
|
|
cm.controller.Shutdown()
|
|
}
|
|
|
|
func (cm *ControllerManager) WaitServerToBeReady(port string) {
|
|
url := GetBaseURL(port)
|
|
WaitTillServerReady(url)
|
|
}
|
|
|
|
func (cm *ControllerManager) StartAndWait(port string) {
|
|
cm.StartServer()
|
|
|
|
url := GetBaseURL(port)
|
|
WaitTillServerReady(url)
|
|
}
|
|
|
|
func NewControllerManager(controller Controller) ControllerManager {
|
|
cm := ControllerManager{
|
|
controller: controller,
|
|
}
|
|
|
|
return cm
|
|
}
|
|
|
|
func WriteImageToFileSystem(image image.Image, repoName, ref string, storeController storage.StoreController) error {
|
|
store := storeController.GetImageStore(repoName)
|
|
|
|
err := store.InitRepo(repoName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, layerBlob := range image.Layers {
|
|
layerReader := bytes.NewReader(layerBlob)
|
|
layerDigest := godigest.FromBytes(layerBlob)
|
|
|
|
_, _, err = store.FullBlobUpload(repoName, layerReader, layerDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
configBlob, err := json.Marshal(image.Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
configReader := bytes.NewReader(configBlob)
|
|
configDigest := godigest.FromBytes(configBlob)
|
|
|
|
_, _, err = store.FullBlobUpload(repoName, configReader, configDigest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
manifestBlob, err := json.Marshal(image.Manifest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = store.PutImageManifest(repoName, ref, ispec.MediaTypeImageManifest, manifestBlob)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func WriteMultiArchImageToFileSystem(multiarchImage image.MultiarchImage, repoName, ref string,
|
|
storeController storage.StoreController,
|
|
) error {
|
|
store := storeController.GetImageStore(repoName)
|
|
|
|
err := store.InitRepo(repoName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, image := range multiarchImage.Images {
|
|
err := WriteImageToFileSystem(image, repoName, image.DigestStr(), storeController)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
indexBlob, err := json.Marshal(multiarchImage.Index)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = store.PutImageManifest(repoName, ref, ispec.MediaTypeImageIndex,
|
|
indexBlob)
|
|
|
|
return err
|
|
}
|
|
|
|
func WaitTillServerReady(url string) {
|
|
for {
|
|
_, err := resty.R().Get(url)
|
|
if err == nil {
|
|
break
|
|
}
|
|
|
|
time.Sleep(SleepTime)
|
|
}
|
|
}
|
|
|
|
func WaitTillTrivyDBDownloadStarted(rootDir string) {
|
|
for {
|
|
if _, err := os.Stat(path.Join(rootDir, "_trivy", "db", "trivy.db")); err == nil {
|
|
break
|
|
}
|
|
|
|
time.Sleep(SleepTime)
|
|
}
|
|
}
|
|
|
|
// Adapted from https://gist.github.com/dopey/c69559607800d2f2f90b1b1ed4e550fb
|
|
func RandomString(n int) string {
|
|
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
|
|
|
|
ret := make([]byte, n)
|
|
|
|
for count := 0; count < n; count++ {
|
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ret[count] = letters[num.Int64()]
|
|
}
|
|
|
|
return string(ret)
|
|
}
|
|
|
|
func GetRandomImageConfig() ([]byte, godigest.Digest) {
|
|
const maxLen = 16
|
|
|
|
randomAuthor := RandomString(maxLen)
|
|
|
|
config := ispec.Image{
|
|
Platform: ispec.Platform{
|
|
Architecture: "amd64",
|
|
OS: "linux",
|
|
},
|
|
RootFS: ispec.RootFS{
|
|
Type: "layers",
|
|
DiffIDs: []godigest.Digest{},
|
|
},
|
|
Author: randomAuthor,
|
|
}
|
|
|
|
configBlobContent, err := json.MarshalIndent(&config, "", "\t")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
configBlobDigestRaw := godigest.FromBytes(configBlobContent)
|
|
|
|
return configBlobContent, configBlobDigestRaw
|
|
}
|
|
|
|
func GetOciLayoutDigests(imagePath string) (godigest.Digest, godigest.Digest, godigest.Digest) {
|
|
var (
|
|
manifestDigest godigest.Digest
|
|
configDigest godigest.Digest
|
|
layerDigest godigest.Digest
|
|
)
|
|
|
|
oci, err := umoci.OpenLayout(imagePath)
|
|
if err != nil {
|
|
panic(fmt.Errorf("error opening layout at '%s' : %w", imagePath, err))
|
|
}
|
|
|
|
defer oci.Close()
|
|
|
|
ctxUmoci := context.Background()
|
|
|
|
index, err := oci.GetIndex(ctxUmoci)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, manifest := range index.Manifests {
|
|
manifestDigest = manifest.Digest
|
|
|
|
manifestBlob, err := oci.GetBlob(ctxUmoci, manifest.Digest)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
manifestBuf, err := io.ReadAll(manifestBlob)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var manifest ispec.Manifest
|
|
|
|
err = json.Unmarshal(manifestBuf, &manifest)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
configDigest = manifest.Config.Digest
|
|
|
|
for _, layer := range manifest.Layers {
|
|
layerDigest = layer.Digest
|
|
}
|
|
}
|
|
|
|
return manifestDigest, configDigest, layerDigest
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manifest, error) {
|
|
config := ispec.Image{
|
|
Platform: ispec.Platform{
|
|
Architecture: "amd64",
|
|
OS: "linux",
|
|
},
|
|
RootFS: ispec.RootFS{
|
|
Type: "layers",
|
|
DiffIDs: []godigest.Digest{},
|
|
},
|
|
Author: "ZotUser",
|
|
}
|
|
|
|
configBlob, err := json.Marshal(config)
|
|
if err = inject.Error(err); err != nil {
|
|
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
|
|
}
|
|
|
|
configDigest := godigest.FromBytes(configBlob)
|
|
|
|
layers := [][]byte{
|
|
make([]byte, layerSize),
|
|
}
|
|
|
|
schemaVersion := 2
|
|
|
|
manifest := ispec.Manifest{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Versioned: specs.Versioned{
|
|
SchemaVersion: schemaVersion,
|
|
},
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: configDigest,
|
|
Size: int64(len(configBlob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: godigest.FromBytes(layers[0]),
|
|
Size: int64(len(layers[0])),
|
|
},
|
|
},
|
|
}
|
|
|
|
return config, layers, manifest, nil
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manifest, error) {
|
|
config := ispec.Image{
|
|
Platform: ispec.Platform{
|
|
Architecture: "amd64",
|
|
OS: "linux",
|
|
},
|
|
RootFS: ispec.RootFS{
|
|
Type: "layers",
|
|
DiffIDs: []godigest.Digest{},
|
|
},
|
|
Author: "ZotUser",
|
|
}
|
|
|
|
configBlob, err := json.Marshal(config)
|
|
if err = inject.Error(err); err != nil {
|
|
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
|
|
}
|
|
|
|
configDigest := godigest.FromBytes(configBlob)
|
|
|
|
layers := [][]byte{
|
|
GetRandomLayer(layerSize),
|
|
}
|
|
|
|
schemaVersion := 2
|
|
|
|
manifest := ispec.Manifest{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Versioned: specs.Versioned{
|
|
SchemaVersion: schemaVersion,
|
|
},
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: configDigest,
|
|
Size: int64(len(configBlob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: godigest.FromBytes(layers[0]),
|
|
Size: int64(len(layers[0])),
|
|
},
|
|
},
|
|
}
|
|
|
|
return config, layers, manifest, nil
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetVulnImageWithConfig(config ispec.Image) (image.Image, error) {
|
|
vulnerableLayer, err := image.GetLayerWithVulnerability()
|
|
if err != nil {
|
|
return image.Image{}, err
|
|
}
|
|
|
|
vulnerableConfig := ispec.Image{
|
|
Platform: config.Platform,
|
|
Config: config.Config,
|
|
RootFS: ispec.RootFS{
|
|
Type: "layers",
|
|
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
|
|
},
|
|
Created: config.Created,
|
|
History: config.History,
|
|
}
|
|
|
|
img, err := GetImageWithComponents(
|
|
vulnerableConfig,
|
|
[][]byte{
|
|
vulnerableLayer,
|
|
})
|
|
if err != nil {
|
|
return image.Image{}, err
|
|
}
|
|
|
|
return img, err
|
|
}
|
|
|
|
func GetRandomLayer(size int) []byte {
|
|
layer := make([]byte, size)
|
|
|
|
_, err := rand.Read(layer)
|
|
if err != nil {
|
|
return layer
|
|
}
|
|
|
|
return layer
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetRandomImage() (image.Image, error) {
|
|
const layerSize = 20
|
|
|
|
config, layers, manifest, err := GetRandomImageComponents(layerSize)
|
|
if err != nil {
|
|
return image.Image{}, err
|
|
}
|
|
|
|
return image.Image{
|
|
Manifest: manifest,
|
|
Layers: layers,
|
|
Config: config,
|
|
}, nil
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetImageComponentsWithConfig(conf ispec.Image) (ispec.Image, [][]byte, ispec.Manifest, error) {
|
|
configBlob, err := json.Marshal(conf)
|
|
if err = inject.Error(err); err != nil {
|
|
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
|
|
}
|
|
|
|
configDigest := godigest.FromBytes(configBlob)
|
|
|
|
layerSize := 100
|
|
layer := make([]byte, layerSize)
|
|
|
|
_, err = rand.Read(layer)
|
|
if err != nil {
|
|
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
|
|
}
|
|
|
|
layers := [][]byte{
|
|
layer,
|
|
}
|
|
|
|
schemaVersion := 2
|
|
|
|
manifest := ispec.Manifest{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Versioned: specs.Versioned{
|
|
SchemaVersion: schemaVersion,
|
|
},
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: configDigest,
|
|
Size: int64(len(configBlob)),
|
|
},
|
|
Layers: []ispec.Descriptor{
|
|
{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: godigest.FromBytes(layers[0]),
|
|
Size: int64(len(layers[0])),
|
|
},
|
|
},
|
|
}
|
|
|
|
return conf, layers, manifest, nil
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetImageWithConfig(conf ispec.Image) (image.Image, error) {
|
|
config, layers, manifest, err := GetImageComponentsWithConfig(conf)
|
|
if err != nil {
|
|
return image.Image{}, err
|
|
}
|
|
|
|
return image.Image{
|
|
Manifest: manifest,
|
|
Config: config,
|
|
Layers: layers,
|
|
}, nil
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetImageWithComponents(config ispec.Image, layers [][]byte) (image.Image, error) {
|
|
configBlob, err := json.Marshal(config)
|
|
if err != nil {
|
|
return image.Image{}, err
|
|
}
|
|
|
|
manifestLayers := make([]ispec.Descriptor, 0, len(layers))
|
|
|
|
for _, layer := range layers {
|
|
manifestLayers = append(manifestLayers, ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.layer.v1.tar",
|
|
Digest: godigest.FromBytes(layer),
|
|
Size: int64(len(layer)),
|
|
})
|
|
}
|
|
|
|
const schemaVersion = 2
|
|
|
|
manifest := ispec.Manifest{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Versioned: specs.Versioned{
|
|
SchemaVersion: schemaVersion,
|
|
},
|
|
Config: ispec.Descriptor{
|
|
MediaType: "application/vnd.oci.image.config.v1+json",
|
|
Digest: godigest.FromBytes(configBlob),
|
|
Size: int64(len(configBlob)),
|
|
},
|
|
Layers: manifestLayers,
|
|
}
|
|
|
|
return image.Image{
|
|
Manifest: manifest,
|
|
Config: config,
|
|
Layers: layers,
|
|
}, nil
|
|
}
|
|
|
|
func GetCosignSignatureTagForManifest(manifest ispec.Manifest) (string, error) {
|
|
manifestBlob, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
manifestDigest := godigest.FromBytes(manifestBlob)
|
|
|
|
return GetCosignSignatureTagForDigest(manifestDigest), nil
|
|
}
|
|
|
|
func GetCosignSignatureTagForDigest(manifestDigest godigest.Digest) string {
|
|
return manifestDigest.Algorithm().String() + "-" + manifestDigest.Encoded() + ".sig"
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetImageWithSubject(subjectDigest godigest.Digest, mediaType string) (image.Image, error) {
|
|
num := 100
|
|
|
|
conf, layers, manifest, err := GetRandomImageComponents(num)
|
|
if err != nil {
|
|
return image.Image{}, err
|
|
}
|
|
|
|
manifest.Subject = &ispec.Descriptor{
|
|
Digest: subjectDigest,
|
|
MediaType: mediaType,
|
|
}
|
|
|
|
return image.Image{
|
|
Manifest: manifest,
|
|
Config: conf,
|
|
Layers: layers,
|
|
}, nil
|
|
}
|
|
|
|
func ReadLogFileAndSearchString(logPath string, stringToMatch string, timeout time.Duration) (bool, error) {
|
|
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
|
defer cancelFunc()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return false, nil
|
|
default:
|
|
content, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if strings.Contains(string(content), stringToMatch) {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func ReadLogFileAndCountStringOccurence(logPath string, stringToMatch string,
|
|
timeout time.Duration, count int,
|
|
) (bool, error) {
|
|
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
|
defer cancelFunc()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return false, nil
|
|
default:
|
|
content, err := os.ReadFile(logPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if strings.Count(string(content), stringToMatch) >= count {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func CopyFile(sourceFilePath, destFilePath string) error {
|
|
destFile, err := os.Create(destFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
sourceFile, err := os.Open(sourceFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sourceFile.Close()
|
|
|
|
if _, err = io.Copy(destFile, sourceFile); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func LoadNotationPath(tdir string) {
|
|
dir.UserConfigDir = filepath.Join(tdir, "notation")
|
|
|
|
// set user libexec
|
|
dir.UserLibexecDir = dir.UserConfigDir
|
|
}
|
|
|
|
func GenerateNotationCerts(tdir string, certName string) error {
|
|
// generate RSA private key
|
|
bits := 2048
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, bits)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyBytes, err := x509.MarshalPKCS8PrivateKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
|
|
|
|
rsaCertTuple := testhelper.GetRSASelfSignedCertTupleWithPK(key, "cert")
|
|
|
|
certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rsaCertTuple.Cert.Raw})
|
|
|
|
// write private key
|
|
relativeKeyPath, relativeCertPath := dir.LocalKeyPath(certName)
|
|
|
|
configFS := dir.ConfigFS()
|
|
|
|
keyPath, err := configFS.SysPath(relativeKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
certPath, err := configFS.SysPath(relativeCertPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := WriteFileWithPermission(keyPath, keyPEM, 0o600, false); err != nil { //nolint:gomnd
|
|
return fmt.Errorf("failed to write key file: %w", err)
|
|
}
|
|
|
|
// write self-signed certificate
|
|
if err := WriteFileWithPermission(certPath, certBytes, 0o644, false); err != nil { //nolint:gomnd
|
|
return fmt.Errorf("failed to write certificate file: %w", err)
|
|
}
|
|
|
|
signingKeys, err := notconfig.LoadSigningKeys()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keySuite := notconfig.KeySuite{
|
|
Name: certName,
|
|
X509KeyPair: ¬config.X509KeyPair{
|
|
KeyPath: keyPath,
|
|
CertificatePath: certPath,
|
|
},
|
|
}
|
|
|
|
// addKeyToSigningKeys
|
|
if Contains(signingKeys.Keys, keySuite.Name) {
|
|
return ErrAlreadyExists
|
|
}
|
|
|
|
signingKeys.Keys = append(signingKeys.Keys, keySuite)
|
|
|
|
// Add to the trust store
|
|
trustStorePath := path.Join(tdir, fmt.Sprintf("notation/truststore/x509/ca/%s", certName))
|
|
|
|
if _, err := os.Stat(filepath.Join(trustStorePath, filepath.Base(certPath))); err == nil {
|
|
return ErrAlreadyExists
|
|
}
|
|
|
|
if err := os.MkdirAll(trustStorePath, 0o755); err != nil { //nolint:gomnd
|
|
return fmt.Errorf("GenerateNotationCerts os.MkdirAll failed: %w", err)
|
|
}
|
|
|
|
trustCertPath := path.Join(trustStorePath, fmt.Sprintf("%s%s", certName, dir.LocalCertificateExtension))
|
|
|
|
err = CopyFile(certPath, trustCertPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save to the SigningKeys.json
|
|
if err := signingKeys.Save(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SignWithNotation(keyName string, reference string, tdir string) error {
|
|
ctx := context.TODO()
|
|
|
|
// getSigner
|
|
var newSigner notation.Signer
|
|
|
|
mediaType := jws.MediaTypeEnvelope
|
|
|
|
// ResolveKey
|
|
signingKeys, err := LoadNotationSigningkeys(tdir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
idx := Index(signingKeys.Keys, keyName)
|
|
if idx < 0 {
|
|
return ErrKeyNotFound
|
|
}
|
|
|
|
key := signingKeys.Keys[idx]
|
|
|
|
if key.X509KeyPair != nil {
|
|
newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// prepareSigningContent
|
|
// getRepositoryClient
|
|
authClient := &auth.Client{
|
|
Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
|
|
return auth.EmptyCredential, nil
|
|
},
|
|
Cache: auth.NewCache(),
|
|
ClientID: "notation",
|
|
}
|
|
|
|
authClient.SetUserAgent("notation/zot_tests")
|
|
|
|
plainHTTP := true
|
|
|
|
// Resolve referance
|
|
ref, err := registry.ParseReference(reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
remoteRepo := &remote.Repository{
|
|
Client: authClient,
|
|
Reference: ref,
|
|
PlainHTTP: plainHTTP,
|
|
}
|
|
|
|
repositoryOpts := notreg.RepositoryOptions{}
|
|
|
|
sigRepo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
|
|
|
|
sigOpts := notation.SignOptions{
|
|
SignerSignOptions: notation.SignerSignOptions{
|
|
SignatureMediaType: mediaType,
|
|
PluginConfig: map[string]string{},
|
|
},
|
|
ArtifactReference: ref.String(),
|
|
}
|
|
|
|
_, err = notation.Sign(ctx, newSigner, sigRepo, sigOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func VerifyWithNotation(reference string, tdir string) error {
|
|
// check if trustpolicy.json exists
|
|
trustpolicyPath := path.Join(tdir, "notation/trustpolicy.json")
|
|
|
|
if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) {
|
|
trustPolicy := `
|
|
{
|
|
"version": "1.0",
|
|
"trustPolicies": [
|
|
{
|
|
"name": "good",
|
|
"registryScopes": [ "*" ],
|
|
"signatureVerification": {
|
|
"level" : "audit"
|
|
},
|
|
"trustStores": ["ca:good"],
|
|
"trustedIdentities": [
|
|
"*"
|
|
]
|
|
}
|
|
]
|
|
}`
|
|
|
|
file, err := os.Create(trustpolicyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
_, err = file.WriteString(trustPolicy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// start verifying signatures
|
|
ctx := context.TODO()
|
|
|
|
// getRepositoryClient
|
|
authClient := &auth.Client{
|
|
Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
|
|
return auth.EmptyCredential, nil
|
|
},
|
|
Cache: auth.NewCache(),
|
|
ClientID: "notation",
|
|
}
|
|
|
|
authClient.SetUserAgent("notation/zot_tests")
|
|
|
|
plainHTTP := true
|
|
|
|
// Resolve referance
|
|
ref, err := registry.ParseReference(reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
remoteRepo := &remote.Repository{
|
|
Client: authClient,
|
|
Reference: ref,
|
|
PlainHTTP: plainHTTP,
|
|
}
|
|
|
|
repositoryOpts := notreg.RepositoryOptions{}
|
|
|
|
repo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
|
|
|
|
manifestDesc, err := repo.Resolve(ctx, ref.Reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ref.ValidateReferenceAsDigest(); err != nil {
|
|
ref.Reference = manifestDesc.Digest.String()
|
|
}
|
|
|
|
// getVerifier
|
|
newVerifier, err := verifier.NewFromConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
remoteRepo = &remote.Repository{
|
|
Client: authClient,
|
|
Reference: ref,
|
|
PlainHTTP: plainHTTP,
|
|
}
|
|
|
|
repo = notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
|
|
|
|
configs := map[string]string{}
|
|
|
|
verifyOpts := notation.VerifyOptions{
|
|
ArtifactReference: ref.String(),
|
|
PluginConfig: configs,
|
|
MaxSignatureAttempts: math.MaxInt64,
|
|
}
|
|
|
|
_, outcomes, err := notation.Verify(ctx, newVerifier, repo, verifyOpts)
|
|
if err != nil || len(outcomes) == 0 {
|
|
return ErrSignatureVerification
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, error) {
|
|
signatures := []godigest.Digest{}
|
|
|
|
ctx := context.TODO()
|
|
|
|
// getSignatureRepository
|
|
ref, err := registry.ParseReference(reference)
|
|
if err != nil {
|
|
return signatures, err
|
|
}
|
|
|
|
plainHTTP := true
|
|
|
|
// getRepositoryClient
|
|
authClient := &auth.Client{
|
|
Credential: func(ctx context.Context, registry string) (auth.Credential, error) {
|
|
return auth.EmptyCredential, nil
|
|
},
|
|
Cache: auth.NewCache(),
|
|
ClientID: "notation",
|
|
}
|
|
|
|
authClient.SetUserAgent("notation/zot_tests")
|
|
|
|
remoteRepo := &remote.Repository{
|
|
Client: authClient,
|
|
Reference: ref,
|
|
PlainHTTP: plainHTTP,
|
|
}
|
|
|
|
sigRepo := notreg.NewRepository(remoteRepo)
|
|
|
|
artifactDesc, err := sigRepo.Resolve(ctx, reference)
|
|
if err != nil {
|
|
return signatures, err
|
|
}
|
|
|
|
err = sigRepo.ListSignatures(ctx, artifactDesc, func(signatureManifests []ispec.Descriptor) error {
|
|
for _, sigManifestDesc := range signatureManifests {
|
|
signatures = append(signatures, sigManifestDesc.Digest)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return signatures, err
|
|
}
|
|
|
|
func LoadNotationSigningkeys(tdir string) (*notconfig.SigningKeys, error) {
|
|
var err error
|
|
|
|
var signingKeysInfo *notconfig.SigningKeys
|
|
|
|
filePath := path.Join(tdir, "notation/signingkeys.json")
|
|
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
// create file
|
|
newSigningKeys := notconfig.NewSigningKeys()
|
|
|
|
newFile, err := os.Create(filePath)
|
|
if err != nil {
|
|
return newSigningKeys, err
|
|
}
|
|
|
|
defer newFile.Close()
|
|
|
|
encoder := json.NewEncoder(newFile)
|
|
encoder.SetIndent("", " ")
|
|
|
|
err = encoder.Encode(newSigningKeys)
|
|
|
|
return newSigningKeys, err
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
err = json.NewDecoder(file).Decode(&signingKeysInfo)
|
|
|
|
return signingKeysInfo, err
|
|
}
|
|
|
|
func LoadNotationConfig(tdir string) (*notconfig.Config, error) {
|
|
var configInfo *notconfig.Config
|
|
|
|
filePath := path.Join(tdir, "notation/signingkeys.json")
|
|
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return configInfo, err
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
err = json.NewDecoder(file).Decode(&configInfo)
|
|
if err != nil {
|
|
return configInfo, err
|
|
}
|
|
|
|
// set default value
|
|
configInfo.SignatureFormat = strings.ToLower(configInfo.SignatureFormat)
|
|
if configInfo.SignatureFormat == "" {
|
|
configInfo.SignatureFormat = "jws"
|
|
}
|
|
|
|
return configInfo, nil
|
|
}
|
|
|
|
func WriteFileWithPermission(path string, data []byte, perm fs.FileMode, overwrite bool) error {
|
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
flag := os.O_WRONLY | os.O_CREATE
|
|
|
|
if overwrite {
|
|
flag |= os.O_TRUNC
|
|
} else {
|
|
flag |= os.O_EXCL
|
|
}
|
|
|
|
file, err := os.OpenFile(path, flag, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = file.Write(data)
|
|
if err != nil {
|
|
file.Close()
|
|
|
|
return err
|
|
}
|
|
|
|
return file.Close()
|
|
}
|
|
|
|
func IsDigestReference(ref string) bool {
|
|
parts := strings.SplitN(ref, "/", 2) //nolint:gomnd
|
|
if len(parts) == 1 {
|
|
return false
|
|
}
|
|
|
|
index := strings.Index(parts[1], "@")
|
|
|
|
return index != -1
|
|
}
|
|
|
|
type isser interface {
|
|
Is(string) bool
|
|
}
|
|
|
|
// Index returns the index of the first occurrence of name in s,
|
|
// or -1 if not present.
|
|
func Index[E isser](s []E, name string) int {
|
|
for i, v := range s {
|
|
if v.Is(name) {
|
|
return i
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|
|
|
|
// Contains reports whether name is present in s.
|
|
func Contains[E isser](s []E, name string) bool {
|
|
return Index(s, name) >= 0
|
|
}
|
|
|
|
func SignImageUsingCosign(repoTag, port string) error {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = os.Chdir(cwd) }()
|
|
|
|
tdir, err := os.MkdirTemp("", "cosign")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer os.RemoveAll(tdir)
|
|
|
|
_ = os.Chdir(tdir)
|
|
|
|
// generate a keypair
|
|
os.Setenv("COSIGN_PASSWORD", "")
|
|
|
|
err = generate.GenerateKeyPairCmd(context.TODO(), "", "cosign", nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
imageURL := fmt.Sprintf("localhost:%s/%s", port, repoTag)
|
|
|
|
const timeoutPeriod = 5
|
|
|
|
// sign the image
|
|
return sign.SignCmd(&options.RootOptions{Verbose: true, Timeout: timeoutPeriod * time.Minute},
|
|
options.KeyOpts{KeyRef: path.Join(tdir, "cosign.key"), PassFunc: generate.GetPass},
|
|
options.SignOptions{
|
|
Registry: options.RegistryOptions{AllowInsecure: true},
|
|
AnnotationOptions: options.AnnotationOptions{Annotations: []string{"tag=1.0"}},
|
|
Upload: true,
|
|
},
|
|
[]string{imageURL})
|
|
}
|
|
|
|
func SignImageUsingNotary(repoTag, port string) error {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = os.Chdir(cwd) }()
|
|
|
|
tdir, err := os.MkdirTemp("", "notation")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer os.RemoveAll(tdir)
|
|
|
|
_ = os.Chdir(tdir)
|
|
|
|
NotationPathLock.Lock()
|
|
defer NotationPathLock.Unlock()
|
|
|
|
LoadNotationPath(tdir)
|
|
|
|
// generate a keypair
|
|
err = GenerateNotationCerts(tdir, "notation-sign-test")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// sign the image
|
|
image := fmt.Sprintf("localhost:%s/%s", port, repoTag)
|
|
|
|
err = SignWithNotation("notation-sign-test", image, tdir)
|
|
|
|
return err
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetRandomMultiarchImageComponents() (ispec.Index, []image.Image, error) {
|
|
const layerSize = 100
|
|
|
|
randomLayer1 := make([]byte, layerSize)
|
|
|
|
_, err := rand.Read(randomLayer1)
|
|
if err != nil {
|
|
return ispec.Index{}, []image.Image{}, err
|
|
}
|
|
|
|
image1, err := GetImageWithComponents(
|
|
ispec.Image{
|
|
Platform: ispec.Platform{
|
|
OS: "linux",
|
|
Architecture: "amd64",
|
|
},
|
|
},
|
|
[][]byte{
|
|
randomLayer1,
|
|
})
|
|
if err != nil {
|
|
return ispec.Index{}, []image.Image{}, err
|
|
}
|
|
|
|
randomLayer2 := make([]byte, layerSize)
|
|
|
|
_, err = rand.Read(randomLayer2)
|
|
if err != nil {
|
|
return ispec.Index{}, []image.Image{}, err
|
|
}
|
|
|
|
image2, err := GetImageWithComponents(
|
|
ispec.Image{
|
|
Platform: ispec.Platform{
|
|
OS: "linux",
|
|
Architecture: "386",
|
|
},
|
|
},
|
|
[][]byte{
|
|
randomLayer2,
|
|
})
|
|
if err != nil {
|
|
return ispec.Index{}, []image.Image{}, err
|
|
}
|
|
|
|
randomLayer3 := make([]byte, layerSize)
|
|
|
|
_, err = rand.Read(randomLayer3)
|
|
if err != nil {
|
|
return ispec.Index{}, []image.Image{}, err
|
|
}
|
|
|
|
image3, err := GetImageWithComponents(
|
|
ispec.Image{
|
|
Platform: ispec.Platform{
|
|
OS: "windows",
|
|
Architecture: "amd64",
|
|
},
|
|
},
|
|
[][]byte{
|
|
randomLayer3,
|
|
})
|
|
if err != nil {
|
|
return ispec.Index{}, []image.Image{}, err
|
|
}
|
|
|
|
index := ispec.Index{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Manifests: []ispec.Descriptor{
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: getManifestDigest(image1.Manifest),
|
|
Size: getManifestSize(image1.Manifest),
|
|
},
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: getManifestDigest(image2.Manifest),
|
|
Size: getManifestSize(image2.Manifest),
|
|
},
|
|
{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: getManifestDigest(image3.Manifest),
|
|
Size: getManifestSize(image3.Manifest),
|
|
},
|
|
},
|
|
}
|
|
|
|
return index, []image.Image{image1, image2, image3}, nil
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetRandomMultiarchImage(reference string) (image.MultiarchImage, error) {
|
|
index, images, err := GetRandomMultiarchImageComponents()
|
|
if err != nil {
|
|
return image.MultiarchImage{}, err
|
|
}
|
|
|
|
index.SchemaVersion = 2
|
|
|
|
return image.MultiarchImage{
|
|
Index: index, Images: images, Reference: reference,
|
|
}, err
|
|
}
|
|
|
|
// Deprecated: Should use the new functions starting with "Create".
|
|
func GetMultiarchImageForImages(images []image.Image) image.MultiarchImage {
|
|
var index ispec.Index
|
|
|
|
for _, image := range images {
|
|
index.Manifests = append(index.Manifests, ispec.Descriptor{
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
Digest: getManifestDigest(image.Manifest),
|
|
Size: getManifestSize(image.Manifest),
|
|
})
|
|
}
|
|
|
|
index.SchemaVersion = 2
|
|
|
|
return image.MultiarchImage{Index: index, Images: images}
|
|
}
|
|
|
|
func getManifestSize(manifest ispec.Manifest) int64 {
|
|
manifestBlob, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
|
|
return int64(len(manifestBlob))
|
|
}
|
|
|
|
func getManifestDigest(manifest ispec.Manifest) godigest.Digest {
|
|
manifestBlob, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return godigest.FromBytes(manifestBlob)
|
|
}
|
|
|
|
func GetIndexBlobWithManifests(manifestDigests []godigest.Digest) ([]byte, error) {
|
|
manifests := make([]ispec.Descriptor, 0, len(manifestDigests))
|
|
|
|
for _, manifestDigest := range manifestDigests {
|
|
manifests = append(manifests, ispec.Descriptor{
|
|
Digest: manifestDigest,
|
|
MediaType: ispec.MediaTypeImageManifest,
|
|
})
|
|
}
|
|
|
|
indexContent := ispec.Index{
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Manifests: manifests,
|
|
}
|
|
|
|
return json.Marshal(indexContent)
|
|
}
|
|
|
|
func MockOIDCRun() (*mockoidc.MockOIDC, error) {
|
|
// Create a fresh RSA Private Key for token signing
|
|
rsaKey, _ := rsa.GenerateKey(rand.Reader, 2048) //nolint: gomnd
|
|
|
|
// Create an unstarted MockOIDC server
|
|
mockServer, _ := mockoidc.NewServer(rsaKey)
|
|
|
|
// Create the net.Listener, kernel will chose a valid port
|
|
listener, _ := net.Listen("tcp", "127.0.0.1:0")
|
|
|
|
bearerMiddleware := func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(response http.ResponseWriter, req *http.Request) {
|
|
// stateVal := req.Form.Get("state")
|
|
header := req.Header.Get("Authorization")
|
|
parts := strings.SplitN(header, " ", 2) //nolint: gomnd
|
|
if header != "" {
|
|
if strings.ToLower(parts[0]) == "bearer" {
|
|
req.Header.Set("Authorization", strings.Join([]string{"Bearer", parts[1]}, " "))
|
|
}
|
|
}
|
|
|
|
next.ServeHTTP(response, req)
|
|
})
|
|
}
|
|
|
|
err := mockServer.AddMiddleware(bearerMiddleware)
|
|
if err != nil {
|
|
return mockServer, err
|
|
}
|
|
// tlsConfig can be nil if you want HTTP
|
|
return mockServer, mockServer.Start(listener, nil)
|
|
}
|
|
|
|
func CustomRedirectPolicy(noOfRedirect int) resty.RedirectPolicy {
|
|
return resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
|
if len(via) >= noOfRedirect {
|
|
return fmt.Errorf("stopped after %d redirects", noOfRedirect) //nolint: goerr113
|
|
}
|
|
|
|
for key, val := range via[len(via)-1].Header {
|
|
req.Header[key] = val
|
|
}
|
|
|
|
respCookies := req.Response.Cookies()
|
|
for _, cookie := range respCookies {
|
|
req.AddCookie(cookie)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func GetDefaultImageStore(rootDir string, log zLog.Logger) stypes.ImageStore {
|
|
return local.NewImageStore(rootDir, false, false, log,
|
|
monitoring.NewMetricsServer(false, log),
|
|
mocks.MockedLint{
|
|
LintFn: func(repo string, manifestDigest godigest.Digest, imageStore stypes.ImageStore) (bool, error) {
|
|
return true, nil
|
|
},
|
|
},
|
|
mocks.CacheMock{},
|
|
)
|
|
}
|
|
|
|
func GetDefaultStoreController(rootDir string, log zLog.Logger) storage.StoreController {
|
|
return storage.StoreController{
|
|
DefaultStore: GetDefaultImageStore(rootDir, log),
|
|
}
|
|
}
|
|
|
|
func RemoveLocalStorageContents(imageStore stypes.ImageStore) error {
|
|
repos, err := imageStore.GetRepositories()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, repo := range repos {
|
|
// take just the first path
|
|
err = os.RemoveAll(filepath.Join(imageStore.RootDir(), filepath.SplitList(repo)[0]))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|