mirror of
https://github.com/kubesphere/kubekey.git
synced 2025-12-25 17:12:50 +00:00
* feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> * feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> * feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> * feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> feat: support user pull one or more arch from image registry and combine them to one image Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com> --------- Signed-off-by: xuesongzuo@yunify.com <xuesongzuo@yunify.com>
1297 lines
38 KiB
Go
1297 lines
38 KiB
Go
/*
|
|
Copyright 2024 The KubeSphere 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 modules
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
"github.com/containerd/containerd/images"
|
|
kkprojectv1 "github.com/kubesphere/kubekey/api/project/v1"
|
|
"github.com/opencontainers/go-digest"
|
|
"github.com/opencontainers/image-spec/specs-go"
|
|
imagev1 "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/utils/ptr"
|
|
"oras.land/oras-go/v2"
|
|
"oras.land/oras-go/v2/registry"
|
|
"oras.land/oras-go/v2/registry/remote"
|
|
"oras.land/oras-go/v2/registry/remote/auth"
|
|
|
|
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
|
"github.com/kubesphere/kubekey/v4/pkg/converter/tmpl"
|
|
"github.com/kubesphere/kubekey/v4/pkg/variable"
|
|
)
|
|
|
|
/*
|
|
The Image module handles container image operations including pulling images from registries and pushing images to private registries.
|
|
|
|
Configuration:
|
|
Users can specify image operations through the following parameters:
|
|
|
|
image:
|
|
pull: # optional: pull configuration
|
|
manifests: []string # required: list of image manifests to pull
|
|
images_dir: string # required: directory to store pulled images
|
|
skipTLSVerify: bool # optional: skip TLS verification
|
|
autus: # optional: target image repo access information, slice type
|
|
- repo: string # optional: target image repo
|
|
username: string # optional: target image repo access username
|
|
password: string # optional: target image repo access password
|
|
push: # optional: push configuration
|
|
username: string # optional: registry username
|
|
password: string # optional: registry password
|
|
images_dir: string # required: directory containing images to push
|
|
skipTLSVerify: bool # optional: skip TLS verification
|
|
src_pattern: string # optional: source image pattern to push (regex supported). If not specified, all images in images_dir will be pushed
|
|
dest: string # required: destination registry and image name. Supports template syntax for dynamic values
|
|
copy:
|
|
from:
|
|
type: string # required: image source type, file or hub
|
|
path: string # optional: when image source type is file, then required, means image file path
|
|
manifests: []string # required: list of image manifests to pull
|
|
skipTLSVerify: bool # optional: skip TLS verification
|
|
autus: # optional: target image repo access information, slice type
|
|
- repo: string # optional: target image repo
|
|
username: string # optional: target image repo access username
|
|
password: string # optional: target image repo access password
|
|
to:
|
|
type: string # required: image target type, file or hub
|
|
path: string # required: image target path
|
|
skipTLSVerify: bool # optional: skip TLS verification
|
|
pattern: string # optional: source image pattern to push (regex supported). If not specified, all images in images_dir will be pushed
|
|
autus: # optional: target image repo access information, slice type
|
|
- repo: string # optional: target image repo
|
|
username: string # optional: target image repo access username
|
|
password: string # optional: target image repo access password
|
|
|
|
Usage Examples in Playbook Tasks:
|
|
1. Pull images from registry:
|
|
```yaml
|
|
- name: Pull container images
|
|
image:
|
|
pull:
|
|
manifests:
|
|
- nginx:latest
|
|
- prometheus:v2.45.0
|
|
images_dir: /path/to/images
|
|
auths:
|
|
- repo: docker.io
|
|
username: MyDockerAccount
|
|
password: my_password
|
|
- repo: my.dockerhub.local
|
|
username: MyHubAccount
|
|
password: my_password
|
|
register: pull_result
|
|
```
|
|
|
|
2. Push images to private registry:
|
|
```yaml
|
|
- name: Push images to private registry
|
|
image:
|
|
push:
|
|
username: admin
|
|
password: secret
|
|
namespace_override: custom-ns
|
|
images_dir: /path/to/images
|
|
dest: registry.example.com/{{ . }}
|
|
register: push_result
|
|
```
|
|
|
|
3. Copy image from file to file
|
|
```yaml
|
|
- name: file to file
|
|
image:
|
|
copy:
|
|
from:
|
|
path: "/path/from/images"
|
|
manifests:
|
|
- nginx:latest
|
|
- prometheus:v2.45.0
|
|
to:
|
|
path: /path/to/images
|
|
```
|
|
|
|
Return Values:
|
|
- On success: Returns "Success" in stdout
|
|
- On failure: Returns error message in stderr
|
|
*/
|
|
|
|
const defaultRegistry = "docker.io"
|
|
|
|
// imageArgs holds the configuration for image operations
|
|
type imageArgs struct {
|
|
pull *imagePullArgs
|
|
push *imagePushArgs
|
|
copy *imageCopyArgs
|
|
}
|
|
|
|
// imagePullArgs contains parameters for pulling images
|
|
type imagePullArgs struct {
|
|
imagesDir string
|
|
manifests []string
|
|
skipTLSVerify *bool
|
|
platform []string
|
|
auths []imageAuth
|
|
}
|
|
|
|
type imageAuth struct {
|
|
Repo string `json:"repo"`
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
Insecure *bool `json:"insecure"`
|
|
PlainHTTP *bool `json:"plain_http"`
|
|
}
|
|
|
|
type fetchResult struct {
|
|
IsIndex bool
|
|
IndexDesc *imagev1.Descriptor
|
|
Index *imagev1.Index
|
|
Manifests []*manifestInfo
|
|
}
|
|
|
|
type manifestInfo struct {
|
|
Desc imagev1.Descriptor
|
|
Content []byte
|
|
Platform *imagev1.Platform
|
|
SourceRepo *remote.Repository
|
|
}
|
|
|
|
// pull retrieves images from a remote registry and stores them locally
|
|
func (i imagePullArgs) pull(ctx context.Context, platform []string) error {
|
|
for _, img := range i.manifests {
|
|
img = normalizeImageNameSimple(img)
|
|
src, err := remote.NewRepository(img)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get remote image %s", img)
|
|
}
|
|
selectedAuth := selectAuth(img, i.auths)
|
|
src.Client = &auth.Client{
|
|
Client: &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: skipTLSVerifyFunc(selectedAuth, *i.skipTLSVerify),
|
|
},
|
|
},
|
|
},
|
|
Cache: auth.NewCache(),
|
|
Credential: authFunc(selectedAuth),
|
|
}
|
|
dst, err := newLocalRepository(filepath.Join(src.Reference.Registry, src.Reference.Repository)+":"+src.Reference.Reference, i.imagesDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
src.PlainHTTP = plainHTTPFunc(selectedAuth, false)
|
|
|
|
if err = imageSrcToDst(ctx, src, dst, img, platform); err != nil {
|
|
return errors.Wrapf(err, "failed to pull image %q to local dir", img)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func imageSrcToDst(ctx context.Context, src, dst *remote.Repository, img string, platform []string) error {
|
|
var err error
|
|
if len(platform) == 0 || (len(platform) == 1 && strings.TrimSpace(platform[0]) == "*") {
|
|
_, err = oras.Copy(ctx, src, src.Reference.Reference, dst, "", oras.DefaultCopyOptions)
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "failed to pull image %q to local dir", img)
|
|
}
|
|
return err
|
|
}
|
|
fetchResult, defaultMediaType, err := fetchManifestsFromMultiArch(ctx, src, src.Reference.Reference)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to fetch manifests")
|
|
}
|
|
if !fetchResult.IsIndex {
|
|
_, err = oras.Copy(ctx, src, src.Reference.Reference, dst, "", oras.DefaultCopyOptions)
|
|
if err != nil {
|
|
err = errors.Wrapf(err, "failed to pull image %q to local dir", img)
|
|
}
|
|
return err
|
|
}
|
|
// filter target platform
|
|
var filteredManifests []*manifestInfo
|
|
for _, manifest := range fetchResult.Manifests {
|
|
// some arm architecture is arm64/v7 or arm68/v8 , support all of then
|
|
for _, arch := range platform {
|
|
if strings.Contains(manifest.Platform.Architecture, arch) {
|
|
manifest.SourceRepo = src
|
|
filteredManifests = append(filteredManifests, manifest)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(filteredManifests) == 0 {
|
|
klog.Warningf("Image %s has no manifests matched for platform: %s", img, platform)
|
|
return nil
|
|
}
|
|
// push all filtered manifests and layers
|
|
for _, manifest := range filteredManifests {
|
|
if err = pushManifestWithLayers(ctx, src, dst, manifest); err != nil {
|
|
return errors.Wrapf(err, "failed to push manifest for %s/%s",
|
|
manifest.Platform.OS, manifest.Platform.Architecture)
|
|
}
|
|
}
|
|
err = createAndPushIndex(ctx, dst, filteredManifests, dst.Reference.Reference, defaultMediaType)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to pull image %q to local dir", img)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func fetchManifestsFromMultiArch(ctx context.Context, repo *remote.Repository, ref string) (*fetchResult, string, error) {
|
|
desc, rc, err := repo.FetchReference(ctx, ref)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to fetch reference %s: %w", ref, err)
|
|
}
|
|
defer rc.Close()
|
|
|
|
content, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to read content: %w", err)
|
|
}
|
|
|
|
result := &fetchResult{}
|
|
|
|
if desc.MediaType == imagev1.MediaTypeImageIndex ||
|
|
desc.MediaType == "application/vnd.docker.distribution.manifest.list.v2+json" {
|
|
// multi arch image
|
|
result.IsIndex = true
|
|
result.IndexDesc = &desc
|
|
|
|
var index imagev1.Index
|
|
if err := json.Unmarshal(content, &index); err != nil {
|
|
return nil, "", fmt.Errorf("failed to unmarshal index: %w", err)
|
|
}
|
|
result.Index = &index
|
|
|
|
for _, manifestDesc := range index.Manifests {
|
|
if manifestDesc.MediaType != imagev1.MediaTypeImageManifest &&
|
|
manifestDesc.MediaType != "application/vnd.docker.distribution.manifest.v2+json" {
|
|
continue
|
|
}
|
|
|
|
manifestInfo, err := fetchSingleManifest(ctx, repo, manifestDesc)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to fetch manifest %s: %w", manifestDesc.Digest, err)
|
|
}
|
|
|
|
result.Manifests = append(result.Manifests, manifestInfo)
|
|
}
|
|
} else if desc.MediaType == imagev1.MediaTypeImageManifest ||
|
|
desc.MediaType == "application/vnd.docker.distribution.manifest.v2+json" {
|
|
// single arch image
|
|
result.IsIndex = false
|
|
info, err := fetchSingleManifestFromContent(content, &desc)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed to parse manifest: %w", err)
|
|
}
|
|
result.Manifests = []*manifestInfo{info}
|
|
} else {
|
|
return nil, "", fmt.Errorf("unsupported media type: %s", desc.MediaType)
|
|
}
|
|
|
|
return result, desc.MediaType, nil
|
|
}
|
|
|
|
func fetchSingleManifest(ctx context.Context, repo *remote.Repository, desc imagev1.Descriptor) (*manifestInfo, error) {
|
|
rc, err := repo.Fetch(ctx, desc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch manifest: %w", err)
|
|
}
|
|
defer rc.Close()
|
|
|
|
content, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read manifest content: %w", err)
|
|
}
|
|
|
|
return fetchSingleManifestFromContent(content, &desc)
|
|
}
|
|
|
|
func fetchSingleManifestFromContent(content []byte, desc *imagev1.Descriptor) (*manifestInfo, error) {
|
|
var manifest imagev1.Manifest
|
|
if err := json.Unmarshal(content, &manifest); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal manifest: %w", err)
|
|
}
|
|
|
|
platform := desc.Platform
|
|
if platform == nil {
|
|
// read platform from config
|
|
// but if config has no platform info ,then use default unknown
|
|
if manifest.Config.Platform != nil {
|
|
platform = manifest.Config.Platform
|
|
} else {
|
|
platform = &imagev1.Platform{
|
|
Architecture: "unknown",
|
|
OS: "unknown",
|
|
}
|
|
}
|
|
}
|
|
|
|
return &manifestInfo{
|
|
Desc: *desc,
|
|
Content: content,
|
|
Platform: platform,
|
|
}, nil
|
|
}
|
|
|
|
func pushManifestWithLayers(ctx context.Context, srcRepo, dstRepo *remote.Repository, manifestInfo *manifestInfo) error {
|
|
var manifest imagev1.Manifest
|
|
if err := json.Unmarshal(manifestInfo.Content, &manifest); err != nil {
|
|
return fmt.Errorf("failed to unmarshal manifest: %w", err)
|
|
}
|
|
|
|
// push config layer
|
|
if err := copyBlob(ctx, srcRepo, dstRepo, manifest.Config); err != nil {
|
|
return fmt.Errorf("failed to copy config: %w", err)
|
|
}
|
|
|
|
// push all layers
|
|
for _, layer := range manifest.Layers {
|
|
if err := copyBlob(ctx, srcRepo, dstRepo, layer); err != nil {
|
|
return fmt.Errorf("failed to copy layer %s: %w", layer.Digest, err)
|
|
}
|
|
}
|
|
|
|
// push manifests
|
|
manifestDesc := imagev1.Descriptor{
|
|
MediaType: manifest.MediaType,
|
|
Digest: digest.FromBytes(manifestInfo.Content),
|
|
Size: int64(len(manifestInfo.Content)),
|
|
Platform: manifestInfo.Platform,
|
|
}
|
|
|
|
exists, err := dstRepo.Exists(ctx, manifestDesc)
|
|
if err == nil && exists {
|
|
return nil
|
|
}
|
|
|
|
err = dstRepo.Push(ctx, manifestDesc, bytes.NewReader(manifestInfo.Content))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to push manifest: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func copyBlob(ctx context.Context, srcRepo, dstRepo *remote.Repository, desc imagev1.Descriptor) error {
|
|
exists, err := dstRepo.Exists(ctx, desc)
|
|
if err == nil && exists {
|
|
return nil
|
|
}
|
|
|
|
rc, err := srcRepo.Fetch(ctx, desc)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to fetch blob %s: %w", desc.Digest, err)
|
|
}
|
|
defer rc.Close()
|
|
|
|
err = dstRepo.Push(ctx, desc, rc)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to push blob %s: %w", desc.Digest, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createAndPushIndex(ctx context.Context, dstRepo *remote.Repository, manifests []*manifestInfo, targetTag, defaultMediaType string) error {
|
|
var descList = make([]imagev1.Descriptor, 0)
|
|
for _, info := range manifests {
|
|
var manifest imagev1.Manifest
|
|
if err := json.Unmarshal(info.Content, &manifest); err != nil {
|
|
return fmt.Errorf("failed to unmarshal manifest: %w", err)
|
|
}
|
|
desc := imagev1.Descriptor{
|
|
MediaType: manifest.MediaType,
|
|
Digest: digest.FromBytes(info.Content),
|
|
Size: int64(len(info.Content)),
|
|
Platform: info.Platform,
|
|
}
|
|
descList = append(descList, desc)
|
|
}
|
|
|
|
index := imagev1.Index{
|
|
Versioned: specs.Versioned{SchemaVersion: 2},
|
|
MediaType: defaultMediaType,
|
|
Manifests: descList,
|
|
Annotations: map[string]string{
|
|
"org.opencontainers.image.created": time.Now().UTC().Format(time.RFC3339),
|
|
"org.opencontainers.image.ref.name": targetTag,
|
|
},
|
|
}
|
|
|
|
indexJSON, err := json.MarshalIndent(index, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal index: %w", err)
|
|
}
|
|
|
|
indexDesc := imagev1.Descriptor{
|
|
MediaType: defaultMediaType,
|
|
Digest: digest.FromBytes(indexJSON),
|
|
Size: int64(len(indexJSON)),
|
|
}
|
|
|
|
err = dstRepo.PushReference(ctx, indexDesc, bytes.NewReader(indexJSON), targetTag)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to push index: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func dockerHostParser(img string) string {
|
|
// if image is like docker.io/xxx/xxx:tag, then download by pull func will store it to registry-1.docker.io
|
|
// so we should change host from docker.io to registry-1.docker.io
|
|
splitedImg := strings.Split(img, "/")
|
|
if len(splitedImg) == 1 {
|
|
return img
|
|
}
|
|
if splitedImg[0] != "docker.io" {
|
|
return img
|
|
}
|
|
splitedImg[0] = "registry-1.docker.io"
|
|
return strings.Join(splitedImg, "/")
|
|
}
|
|
|
|
func authFunc(selectedAuth *imageAuth) func(ctx context.Context, hostport string) (auth.Credential, error) {
|
|
cred := auth.Credential{}
|
|
if selectedAuth != nil {
|
|
cred.Username = selectedAuth.Username
|
|
cred.Password = selectedAuth.Password
|
|
}
|
|
return func(_ context.Context, _ string) (auth.Credential, error) {
|
|
return cred, nil
|
|
}
|
|
}
|
|
|
|
func selectAuth(image string, authList []imageAuth) *imageAuth {
|
|
// remove tag and hash
|
|
repoPart := extractRepoFromImage(image)
|
|
|
|
// parse image to url
|
|
imageURL, err := normalizeImageToURL(repoPart)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
imageHost := imageURL.Host
|
|
imagePath := strings.TrimPrefix(imageURL.Path, "/")
|
|
|
|
// split port
|
|
imageHostWithoutPort := imageHost
|
|
if colonIdx := strings.Index(imageHost, ":"); colonIdx != -1 {
|
|
imageHostWithoutPort = imageHost[:colonIdx]
|
|
}
|
|
|
|
var bestMatch *imageAuth
|
|
var bestMatchScore = -1
|
|
|
|
// find biggest match point auth
|
|
for i := range authList {
|
|
authRepo := authList[i].Repo
|
|
|
|
authURL, err := normalizeImageToURL(authRepo)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
authHost := authURL.Host
|
|
authPath := strings.TrimPrefix(authURL.Path, "/")
|
|
|
|
authHostWithoutPort := authHost
|
|
if colonIdx := strings.Index(authHost, ":"); colonIdx != -1 {
|
|
authHostWithoutPort = authHost[:colonIdx]
|
|
}
|
|
|
|
score := calculateMatchScore(imageHost, imageHostWithoutPort, imagePath,
|
|
authHost, authHostWithoutPort, authPath)
|
|
|
|
if score > bestMatchScore {
|
|
bestMatchScore = score
|
|
bestMatch = &authList[i]
|
|
}
|
|
}
|
|
|
|
return bestMatch
|
|
}
|
|
|
|
func normalizeImageToURL(image string) (*url.URL, error) {
|
|
if strings.HasPrefix(image, "http://") || strings.HasPrefix(image, "https://") {
|
|
return url.Parse(image)
|
|
}
|
|
return url.Parse("http://" + image)
|
|
}
|
|
|
|
// extractRepoFromImage handle image ,ignore tag and hash
|
|
// like xxx/xxx:tag@sha256:xxx to xxx/xxx
|
|
// like xxx/xxx:tag to xxx/xxx
|
|
func extractRepoFromImage(image string) string {
|
|
if image == "" {
|
|
return ""
|
|
}
|
|
|
|
repoPart := image
|
|
|
|
// handle image with hash
|
|
atIdx := strings.LastIndex(repoPart, "@")
|
|
if atIdx != -1 && atIdx > 0 {
|
|
afterAt := repoPart[atIdx+1:]
|
|
if isLikelyDigest(afterAt) {
|
|
repoPart = repoPart[:atIdx]
|
|
}
|
|
}
|
|
|
|
// search /
|
|
firstSlashIdx := strings.Index(repoPart, "/")
|
|
|
|
if firstSlashIdx == -1 {
|
|
return repoPart
|
|
}
|
|
|
|
// search : for tag or port
|
|
lastColonIdx := strings.LastIndex(repoPart, ":")
|
|
if lastColonIdx == -1 {
|
|
return repoPart
|
|
}
|
|
|
|
if lastColonIdx < firstSlashIdx {
|
|
return repoPart
|
|
}
|
|
|
|
if lastColonIdx+1 < len(repoPart) {
|
|
afterColon := repoPart[lastColonIdx+1:]
|
|
if strings.Contains(afterColon, "/") {
|
|
return repoPart
|
|
}
|
|
}
|
|
return repoPart[:lastColonIdx]
|
|
}
|
|
|
|
func isLikelyDigest(s string) bool {
|
|
colonIdx := strings.Index(s, ":")
|
|
if colonIdx == -1 {
|
|
return false
|
|
}
|
|
algorithm := s[:colonIdx]
|
|
knownAlgorithms := []string{"sha256", "sha512", "sha384", "sha1", "md5"}
|
|
for _, algo := range knownAlgorithms {
|
|
if algorithm == algo {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// calculateMatchScore calculate image and path match point
|
|
// 0 for not matched
|
|
// 1 for host matched
|
|
// 2 for host:port matched
|
|
// 3 for host:port and prefix path matched
|
|
// 4 for host:port and full path matched
|
|
func calculateMatchScore(imgHost, imgHostNoPort, imgPath,
|
|
authHost, authHostNoPort, authPath string) int {
|
|
if imgHostNoPort != authHostNoPort {
|
|
return 0
|
|
}
|
|
score := 1
|
|
imgHasPort := strings.Contains(imgHost, ":")
|
|
authHasPort := strings.Contains(authHost, ":")
|
|
if imgHasPort && authHasPort {
|
|
if imgHost != authHost {
|
|
return 1
|
|
}
|
|
score = 2
|
|
} else if !imgHasPort && !authHasPort {
|
|
score = 2
|
|
}
|
|
if authPath == "" {
|
|
return score
|
|
}
|
|
if imgPath == authPath {
|
|
return score + 2
|
|
}
|
|
if strings.HasPrefix(imgPath, authPath) {
|
|
if len(imgPath) == len(authPath) ||
|
|
(len(imgPath) > len(authPath) && imgPath[len(authPath)] == '/') {
|
|
return score + 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func skipTLSVerifyFunc(selectedAuth *imageAuth, defaults bool) bool {
|
|
if selectedAuth != nil && selectedAuth.Insecure != nil {
|
|
return *selectedAuth.Insecure
|
|
}
|
|
return defaults
|
|
}
|
|
|
|
func plainHTTPFunc(selectedAuth *imageAuth, defaults bool) bool {
|
|
if selectedAuth != nil && selectedAuth.PlainHTTP != nil {
|
|
return *selectedAuth.PlainHTTP
|
|
}
|
|
return defaults
|
|
}
|
|
|
|
// imagePushArgs contains parameters for pushing images
|
|
type imagePushArgs struct {
|
|
imagesDir string
|
|
skipTLSVerify *bool
|
|
srcPattern *regexp.Regexp
|
|
destTmpl string
|
|
auths []imageAuth
|
|
}
|
|
|
|
// push uploads local images to a remote registry
|
|
func (i imagePushArgs) push(ctx context.Context, hostVars map[string]any) error {
|
|
manifests, err := findLocalImageManifests(i.imagesDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
klog.V(5).Info("manifests found", "manifests", manifests)
|
|
|
|
for _, img := range manifests {
|
|
// match regex by src
|
|
if i.srcPattern != nil && !i.srcPattern.MatchString(img) {
|
|
// skip
|
|
continue
|
|
}
|
|
src, err := newLocalRepository(img, i.imagesDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dest := i.destTmpl
|
|
if kkprojectv1.IsTmplSyntax(dest) {
|
|
// add temporary variable
|
|
_ = unstructured.SetNestedField(hostVars, src.Reference.Registry, "module", "image", "src", "reference", "registry")
|
|
_ = unstructured.SetNestedField(hostVars, src.Reference.Repository, "module", "image", "src", "reference", "repository")
|
|
_ = unstructured.SetNestedField(hostVars, src.Reference.Reference, "module", "image", "src", "reference", "reference")
|
|
dest, err = tmpl.ParseFunc(hostVars, dest, func(b []byte) string { return string(b) })
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
dst, err := remote.NewRepository(dest)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get remote repository %q", dest)
|
|
}
|
|
selectedAuth := selectAuth(dest, i.auths)
|
|
dst.Client = &auth.Client{
|
|
Client: &http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: skipTLSVerifyFunc(selectedAuth, *i.skipTLSVerify),
|
|
},
|
|
},
|
|
},
|
|
Cache: auth.NewCache(),
|
|
Credential: authFunc(selectedAuth),
|
|
}
|
|
|
|
dst.PlainHTTP = plainHTTPFunc(selectedAuth, false)
|
|
|
|
if _, err = oras.Copy(ctx, src, src.Reference.Reference, dst, dst.Reference.Reference, oras.DefaultCopyOptions); err != nil {
|
|
return errors.Wrapf(err, "failed to push image %q to remote", img)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type imageCopyArgs struct {
|
|
Platform []string `json:"platform"`
|
|
From imageCopyTargetArgs `json:"from"`
|
|
To imageCopyTargetArgs `json:"to"`
|
|
}
|
|
|
|
type imageCopyTargetArgs struct {
|
|
Path string `json:"path"`
|
|
manifests []string
|
|
Pattern *regexp.Regexp
|
|
}
|
|
|
|
func (i *imageCopyArgs) parseFromVars(vars, cp map[string]any) error {
|
|
i.Platform, _ = variable.StringSliceVar(vars, cp, "platform")
|
|
|
|
i.From.manifests, _ = variable.StringSliceVar(vars, cp, "from", "manifests")
|
|
|
|
i.From.Path, _ = variable.StringVar(vars, cp, "from", "path")
|
|
|
|
toPath, _ := variable.PrintVar(cp, "to", "path")
|
|
if destStr, ok := toPath.(string); !ok {
|
|
return errors.New("\"copy.to.path\" must be a string")
|
|
} else if destStr == "" {
|
|
return errors.New("\"copy.to.path\" should not be empty")
|
|
} else {
|
|
i.To.Path = destStr
|
|
}
|
|
srcPattern, _ := variable.StringVar(vars, cp, "to", "src_pattern")
|
|
if srcPattern != "" {
|
|
pattern, err := regexp.Compile(srcPattern)
|
|
if err != nil {
|
|
return errors.Wrap(err, "\"to.pattern\" should be a valid regular expression. ")
|
|
}
|
|
i.To.Pattern = pattern
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (i *imageCopyArgs) copy(ctx context.Context, hostVars map[string]any) error {
|
|
if sts, err := os.Stat(i.From.Path); err != nil || !sts.IsDir() {
|
|
return errors.New("\"copy.from.path\" must be a exist directory")
|
|
}
|
|
for _, img := range i.From.manifests {
|
|
img = normalizeImageNameSimple(img)
|
|
if i.To.Pattern != nil && !i.To.Pattern.MatchString(img) {
|
|
// skip
|
|
continue
|
|
}
|
|
src, err := newLocalRepository(dockerHostParser(img), i.From.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dest := i.To.Path
|
|
if kkprojectv1.IsTmplSyntax(dest) {
|
|
// add temporary variable
|
|
_ = unstructured.SetNestedField(hostVars, src.Reference.Registry, "module", "image", "src", "reference", "registry")
|
|
_ = unstructured.SetNestedField(hostVars, src.Reference.Repository, "module", "image", "src", "reference", "repository")
|
|
_ = unstructured.SetNestedField(hostVars, src.Reference.Reference, "module", "image", "src", "reference", "reference")
|
|
dest, err = tmpl.ParseFunc(hostVars, dest, func(b []byte) string { return string(b) })
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
dst, err := newLocalRepository(filepath.Join(src.Reference.Registry, src.Reference.Repository)+":"+src.Reference.Reference, dest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = imageSrcToDst(ctx, src, dst, img, i.Platform)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// newImageArgs creates a new imageArgs instance from raw configuration
|
|
func newImageArgs(_ context.Context, raw runtime.RawExtension, vars map[string]any) (*imageArgs, error) {
|
|
ia := &imageArgs{}
|
|
// check args
|
|
args := variable.Extension2Variables(raw)
|
|
binaryDir, _ := variable.StringVar(vars, args, _const.BinaryDir)
|
|
|
|
if pullArgs, ok := args["pull"]; ok {
|
|
pull, ok := pullArgs.(map[string]any)
|
|
if !ok {
|
|
return nil, errors.New("\"pull\" should be map")
|
|
}
|
|
ipl := &imagePullArgs{}
|
|
ipl.manifests, _ = variable.StringSliceVar(vars, pull, "manifests")
|
|
ipl.auths = make([]imageAuth, 0)
|
|
pullAuths := make([]imageAuth, 0)
|
|
_ = variable.AnyVar(vars, pull, &pullAuths, "auths")
|
|
for _, a := range pullAuths {
|
|
a.Repo, _ = tmpl.ParseFunc(vars, a.Repo, func(b []byte) string { return string(b) })
|
|
a.Username, _ = tmpl.ParseFunc(vars, a.Username, func(b []byte) string { return string(b) })
|
|
a.Password, _ = tmpl.ParseFunc(vars, a.Password, func(b []byte) string { return string(b) })
|
|
ipl.auths = append(ipl.auths, a)
|
|
}
|
|
ipl.imagesDir, _ = variable.StringVar(vars, pull, "images_dir")
|
|
ipl.skipTLSVerify, _ = variable.BoolVar(vars, pull, "skip_tls_verify")
|
|
if ipl.skipTLSVerify == nil {
|
|
ipl.skipTLSVerify = ptr.To(false)
|
|
}
|
|
ipl.platform, _ = variable.StringSliceVar(vars, pull, "platform")
|
|
// check args
|
|
if len(ipl.manifests) == 0 {
|
|
return nil, errors.New("\"pull.manifests\" is required")
|
|
}
|
|
if ipl.imagesDir == "" {
|
|
if binaryDir == "" {
|
|
return nil, errors.New("\"pull.images_dir\" is required")
|
|
}
|
|
ipl.imagesDir = filepath.Join(binaryDir, _const.BinaryImagesDir)
|
|
}
|
|
ia.pull = ipl
|
|
}
|
|
// if namespace_override is not empty, it will override the image manifests namespace_override. (namespace maybe multi sub path)
|
|
// push to private registry
|
|
if pushArgs, ok := args["push"]; ok {
|
|
push, ok := pushArgs.(map[string]any)
|
|
if !ok {
|
|
return nil, errors.New("\"push\" should be map")
|
|
}
|
|
|
|
ips := &imagePushArgs{}
|
|
ips.auths = make([]imageAuth, 0)
|
|
pullAuths := make([]imageAuth, 0)
|
|
_ = variable.AnyVar(vars, push, &pullAuths, "auths")
|
|
for _, a := range pullAuths {
|
|
a.Repo, _ = tmpl.ParseFunc(vars, a.Repo, func(b []byte) string { return string(b) })
|
|
a.Username, _ = tmpl.ParseFunc(vars, a.Username, func(b []byte) string { return string(b) })
|
|
a.Password, _ = tmpl.ParseFunc(vars, a.Password, func(b []byte) string { return string(b) })
|
|
ips.auths = append(ips.auths, a)
|
|
}
|
|
ips.imagesDir, _ = variable.StringVar(vars, push, "images_dir")
|
|
srcPattern, _ := variable.StringVar(vars, push, "src_pattern")
|
|
destTmpl, _ := variable.PrintVar(push, "dest")
|
|
ips.skipTLSVerify, _ = variable.BoolVar(vars, push, "skip_tls_verify")
|
|
if ips.skipTLSVerify == nil {
|
|
ips.skipTLSVerify = ptr.To(false)
|
|
}
|
|
// check args
|
|
if ips.imagesDir == "" {
|
|
if binaryDir == "" {
|
|
return nil, errors.New("\"push.images_dir\" is required")
|
|
}
|
|
ips.imagesDir = filepath.Join(binaryDir, _const.BinaryImagesDir)
|
|
}
|
|
if srcPattern != "" {
|
|
pattern, err := regexp.Compile(srcPattern)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "\"push.src\" should be a valid regular expression. ")
|
|
}
|
|
ips.srcPattern = pattern
|
|
}
|
|
if destStr, ok := destTmpl.(string); !ok {
|
|
return nil, errors.New("\"push.dest\" must be a string")
|
|
} else if destStr == "" {
|
|
return nil, errors.New("\"push.dest\" should not be empty")
|
|
} else {
|
|
ips.destTmpl = destStr
|
|
}
|
|
ia.push = ips
|
|
}
|
|
|
|
if cpArgs, ok := args["copy"]; ok {
|
|
cp, ok := cpArgs.(map[string]any)
|
|
if !ok {
|
|
return nil, errors.New("\"copy\" should be map")
|
|
}
|
|
|
|
cps := &imageCopyArgs{}
|
|
|
|
err := cps.parseFromVars(vars, cp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ia.copy = cps
|
|
}
|
|
|
|
return ia, nil
|
|
}
|
|
|
|
// ModuleImage handles the "image" module, managing container image operations including pulling and pushing images
|
|
func ModuleImage(ctx context.Context, options ExecOptions) (string, string, error) {
|
|
// get host variable
|
|
ha, err := options.getAllVariables()
|
|
if err != nil {
|
|
return StdoutFailed, StderrGetHostVariable, err
|
|
}
|
|
|
|
ia, err := newImageArgs(ctx, options.Args, ha)
|
|
if err != nil {
|
|
return StdoutFailed, StderrParseArgument, err
|
|
}
|
|
|
|
// pull image manifests to local dir
|
|
if ia.pull != nil {
|
|
if err := ia.pull.pull(ctx, ia.pull.platform); err != nil {
|
|
return StdoutFailed, "failed to pull image", err
|
|
}
|
|
}
|
|
// push image to private registry
|
|
if ia.push != nil {
|
|
if err := ia.push.push(ctx, ha); err != nil {
|
|
return StdoutFailed, "failed to push image", err
|
|
}
|
|
}
|
|
|
|
if ia.copy != nil {
|
|
if err := ia.copy.copy(ctx, ha); err != nil {
|
|
return StdoutFailed, "failed to push image", err
|
|
}
|
|
}
|
|
|
|
return StdoutSuccess, "", nil
|
|
}
|
|
|
|
// findLocalImageManifests get image manifests with whole image's name.
|
|
func findLocalImageManifests(localDir string) ([]string, error) {
|
|
if _, err := os.Stat(localDir); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to stat local directory", "image_dir", localDir)
|
|
// images is not exist, skip
|
|
return make([]string, 0), nil
|
|
}
|
|
|
|
var manifests []string
|
|
if err := filepath.WalkDir(localDir, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to walkdir %s", path)
|
|
}
|
|
|
|
if path == filepath.Join(localDir, "blobs") {
|
|
return filepath.SkipDir
|
|
}
|
|
|
|
if d.IsDir() || filepath.Base(path) == "manifests" {
|
|
return nil
|
|
}
|
|
|
|
file, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to read file %q", path)
|
|
}
|
|
|
|
var data map[string]any
|
|
if err := json.Unmarshal(file, &data); err != nil {
|
|
klog.V(4).ErrorS(err, "unmarshal manifests file error", "file", path)
|
|
// skip un-except file (empty)
|
|
return nil
|
|
}
|
|
|
|
mediaType, ok := data["mediaType"].(string)
|
|
if !ok {
|
|
return errors.New("invalid mediaType")
|
|
}
|
|
if mediaType == imagev1.MediaTypeImageIndex || mediaType == imagev1.MediaTypeImageManifest || // oci multi or single schema
|
|
mediaType == images.MediaTypeDockerSchema2ManifestList || mediaType == images.MediaTypeDockerSchema2Manifest { // docker multi or single schema
|
|
subpath, err := filepath.Rel(localDir, path)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get relative filepath")
|
|
}
|
|
if strings.HasPrefix(filepath.Base(subpath), "sha256:") {
|
|
// only found image tag.
|
|
return nil
|
|
}
|
|
// the parent dir of subpath is "manifests". should delete it
|
|
manifests = append(manifests, filepath.Dir(filepath.Dir(subpath))+":"+filepath.Base(subpath))
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return manifests, nil
|
|
}
|
|
|
|
// newLocalRepository local dir images repository
|
|
func newLocalRepository(reference, localDir string) (*remote.Repository, error) {
|
|
ref, err := registry.ParseReference(reference)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to parse reference %q", reference)
|
|
}
|
|
// store in each domain
|
|
|
|
return &remote.Repository{
|
|
Reference: ref,
|
|
Client: &http.Client{Transport: &imageTransport{baseDir: localDir}},
|
|
}, nil
|
|
}
|
|
|
|
var responseNotFound = &http.Response{Proto: "Local", StatusCode: http.StatusNotFound}
|
|
var responseNotAllowed = &http.Response{Proto: "Local", StatusCode: http.StatusMethodNotAllowed}
|
|
var responseServerError = &http.Response{Proto: "Local", StatusCode: http.StatusInternalServerError}
|
|
var responseCreated = &http.Response{Proto: "Local", StatusCode: http.StatusCreated}
|
|
var responseOK = &http.Response{Proto: "Local", StatusCode: http.StatusOK}
|
|
|
|
// const domain = "internal"
|
|
const apiPrefix = "/v2/"
|
|
|
|
type imageTransport struct {
|
|
baseDir string
|
|
}
|
|
|
|
// RoundTrip deal http.Request in local dir images.
|
|
func (i imageTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
var resp *http.Response
|
|
|
|
switch request.Method {
|
|
case http.MethodHead:
|
|
resp = i.head(request)
|
|
case http.MethodPost:
|
|
resp = i.post(request)
|
|
case http.MethodPut:
|
|
resp = i.put(request)
|
|
case http.MethodGet:
|
|
resp = i.get(request)
|
|
default:
|
|
resp = responseNotAllowed
|
|
}
|
|
|
|
if resp != nil {
|
|
resp.Request = request
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// head method for http.MethodHead. check if file is exist in blobs dir or manifests dir
|
|
func (i imageTransport) head(request *http.Request) *http.Response {
|
|
if strings.HasSuffix(filepath.Dir(request.URL.Path), "blobs") { // blobs
|
|
filename := filepath.Join(i.baseDir, "blobs", filepath.Base(request.URL.Path))
|
|
if _, err := os.Stat(filename); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to stat blobs", "filename", filename)
|
|
|
|
return responseNotFound
|
|
}
|
|
|
|
return responseOK
|
|
} else if strings.HasSuffix(filepath.Dir(request.URL.Path), "manifests") { // manifests
|
|
filename := filepath.Join(i.baseDir, request.Host, strings.TrimPrefix(request.URL.Path, apiPrefix))
|
|
if _, err := os.Stat(filename); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to stat blobs", "filename", filename)
|
|
|
|
return responseNotFound
|
|
}
|
|
|
|
file, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
klog.V(4).ErrorS(err, "failed to read file", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
var data map[string]any
|
|
if err := json.Unmarshal(file, &data); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to unmarshal file", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
mediaType, ok := data["mediaType"].(string)
|
|
if !ok {
|
|
klog.V(4).ErrorS(nil, "unknown mediaType", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
return &http.Response{
|
|
Proto: "Local",
|
|
StatusCode: http.StatusOK,
|
|
Header: http.Header{
|
|
"Content-Type": []string{mediaType},
|
|
},
|
|
ContentLength: int64(len(file)),
|
|
}
|
|
}
|
|
|
|
return responseNotAllowed
|
|
}
|
|
|
|
// post method for http.MethodPost, accept request.
|
|
func (i imageTransport) post(request *http.Request) *http.Response {
|
|
if strings.HasSuffix(request.URL.Path, "/uploads/") {
|
|
return &http.Response{
|
|
Proto: "Local",
|
|
StatusCode: http.StatusAccepted,
|
|
Header: http.Header{
|
|
"Location": []string{filepath.Dir(request.URL.Path)},
|
|
},
|
|
Request: request,
|
|
}
|
|
}
|
|
|
|
return responseNotAllowed
|
|
}
|
|
|
|
// put method for http.MethodPut, create file in blobs dir or manifests dir
|
|
func (i imageTransport) put(request *http.Request) *http.Response {
|
|
if strings.HasSuffix(request.URL.Path, "/uploads") { // blobs
|
|
defer request.Body.Close()
|
|
|
|
filename := filepath.Join(i.baseDir, "blobs", request.URL.Query().Get("digest"))
|
|
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to create dir", "dir", filepath.Dir(filename))
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
|
|
if err != nil {
|
|
klog.V(4).ErrorS(err, "failed to create file", "filename", filename)
|
|
return responseServerError
|
|
}
|
|
|
|
defer func() {
|
|
if err = file.Sync(); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to sync file", "filename", filename)
|
|
}
|
|
if err = file.Close(); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to close file", "filename", filename)
|
|
}
|
|
}()
|
|
|
|
if _, err = io.Copy(file, request.Body); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to write file", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
return responseCreated
|
|
} else if strings.HasSuffix(filepath.Dir(request.URL.Path), "/manifests") { // manifests
|
|
body, err := io.ReadAll(request.Body)
|
|
if err != nil {
|
|
klog.V(4).ErrorS(err, "failed to read request")
|
|
|
|
return responseServerError
|
|
}
|
|
defer request.Body.Close()
|
|
|
|
filename := filepath.Join(i.baseDir, request.Host, strings.TrimPrefix(request.URL.Path, apiPrefix))
|
|
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to create dir", "dir", filepath.Dir(filename))
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
if err := os.WriteFile(filename, body, os.ModePerm); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to write file", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
return responseCreated
|
|
}
|
|
|
|
return responseNotAllowed
|
|
}
|
|
|
|
// get method for http.MethodGet, get file in blobs dir or manifest dir
|
|
func (i imageTransport) get(request *http.Request) *http.Response {
|
|
if strings.HasSuffix(filepath.Dir(request.URL.Path), "blobs") { // blobs
|
|
filename := filepath.Join(i.baseDir, "blobs", filepath.Base(request.URL.Path))
|
|
if _, err := os.Stat(filename); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to stat blobs", "filename", filename)
|
|
|
|
return responseNotFound
|
|
}
|
|
|
|
file, err := os.Open(filename)
|
|
if err != nil {
|
|
klog.V(4).ErrorS(err, "failed to read file", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
fStat, err := file.Stat()
|
|
if err != nil {
|
|
klog.V(4).ErrorS(err, "failed to read file", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
return &http.Response{
|
|
Proto: "Local",
|
|
StatusCode: http.StatusOK,
|
|
ContentLength: fStat.Size(),
|
|
Body: file,
|
|
}
|
|
} else if strings.HasSuffix(filepath.Dir(request.URL.Path), "manifests") { // manifests
|
|
filename := filepath.Join(i.baseDir, request.Host, strings.TrimPrefix(request.URL.Path, apiPrefix))
|
|
if _, err := os.Stat(filename); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to stat blobs", "filename", filename)
|
|
|
|
return responseNotFound
|
|
}
|
|
|
|
file, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
klog.V(4).ErrorS(err, "failed to read file", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
var data map[string]any
|
|
if err := json.Unmarshal(file, &data); err != nil {
|
|
klog.V(4).ErrorS(err, "failed to unmarshal file data", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
mediaType, ok := data["mediaType"].(string)
|
|
if !ok {
|
|
klog.V(4).ErrorS(nil, "unknown mediaType", "filename", filename)
|
|
|
|
return responseServerError
|
|
}
|
|
|
|
return &http.Response{
|
|
Proto: "Local",
|
|
StatusCode: http.StatusOK,
|
|
Header: http.Header{
|
|
"Content-Type": []string{mediaType},
|
|
},
|
|
ContentLength: int64(len(file)),
|
|
Body: io.NopCloser(bytes.NewReader(file)),
|
|
}
|
|
}
|
|
|
|
return responseNotAllowed
|
|
}
|
|
|
|
func normalizeImageNameSimple(image string) string {
|
|
parts := strings.Split(image, "/")
|
|
|
|
switch len(parts) {
|
|
case 1:
|
|
// image like: ubuntu -> docker.io/library/ubuntu
|
|
return fmt.Sprintf("%s/library/%s", defaultRegistry, image)
|
|
case 2:
|
|
// image like: project/xx or registry/project
|
|
firstPart := parts[0]
|
|
if firstPart == "localhost" || (strings.Contains(firstPart, ".") || strings.Contains(firstPart, ":")) {
|
|
return image
|
|
}
|
|
return fmt.Sprintf("%s/%s", defaultRegistry, image)
|
|
default:
|
|
// image like: registry/project/xx/sub
|
|
firstPart := parts[0]
|
|
if firstPart == "localhost" || (strings.Contains(firstPart, ".") || strings.Contains(firstPart, ":")) {
|
|
return image
|
|
}
|
|
return fmt.Sprintf("%s/%s", defaultRegistry, image)
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
utilruntime.Must(RegisterModule("image", ModuleImage))
|
|
}
|