diff --git a/cmd/metal-api/internal/service/image-service.go b/cmd/metal-api/internal/service/image-service.go index a647d569..da06f905 100644 --- a/cmd/metal-api/internal/service/image-service.go +++ b/cmd/metal-api/internal/service/image-service.go @@ -5,7 +5,9 @@ import ( "fmt" "log/slog" "net/http" + "net/url" "strconv" + "strings" "time" "github.com/metal-stack/metal-api/cmd/metal-api/internal/datastore" @@ -16,6 +18,9 @@ import ( restfulspec "github.com/emicklei/go-restful-openapi/v2" restful "github.com/emicklei/go-restful/v3" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/metal-stack/metal-lib/httperrors" ) @@ -317,7 +322,7 @@ func (r *imageResource) createImage(request *restful.Request, response *restful. } } - err = checkImageURL(requestPayload.ID, requestPayload.URL) + err = checkImageURL(requestPayload.ID, requestPayload.URL, nil) if err != nil { r.sendError(request, response, httperrors.BadRequest(err)) return @@ -346,15 +351,41 @@ func (r *imageResource) createImage(request *restful.Request, response *restful. r.send(request, response, http.StatusCreated, v1.NewImageResponse(img)) } -func checkImageURL(id, url string) error { - // nolint - res, err := http.Head(url) - if err != nil { - return fmt.Errorf("image:%s is not accessible under:%s error:%w", id, url, err) - } - if res.StatusCode >= 400 { - return fmt.Errorf("image:%s is not accessible under:%s status:%s", id, url, res.Status) +func checkImageURL(id, inputURL string, ociCredentials authn.Authenticator) error { + parsedURL := strings.Split(inputURL, "://") + + switch parsedURL[0] { + case "http", "https": + _, err := url.ParseRequestURI(inputURL) + if err != nil { + return fmt.Errorf("image:%s could not be parsed. error:%w", inputURL, err) + } + + res, err := http.Head(inputURL) + if err != nil { + return fmt.Errorf("image:%s is not accessible under:%s error:%w", id, inputURL, err) + } + if res.StatusCode >= 400 { + return fmt.Errorf("image:%s is not accessible under:%s status:%s", id, inputURL, res.Status) + } + case "oci": + ref, err := name.ParseReference(parsedURL[1]) + if err != nil { + return fmt.Errorf("image reference:%s could not be parsed. error:%w", inputURL, err) + } + + if ociCredentials == nil { + ociCredentials = authn.Anonymous + } + + _, err = remote.Image(ref, remote.WithAuth(ociCredentials)) + if err != nil { + return fmt.Errorf("image:%s is not accessible under:%s error:%w", id, inputURL, err) + } + default: + return fmt.Errorf("image:%s with url:%s has unkown protocol", id, inputURL) } + return nil } @@ -411,7 +442,7 @@ func (r *imageResource) updateImage(request *restful.Request, response *restful. newImage.Description = *requestPayload.Description } if requestPayload.URL != nil { - err = checkImageURL(requestPayload.ID, *requestPayload.URL) + err = checkImageURL(requestPayload.ID, *requestPayload.URL, nil) if err != nil { r.sendError(request, response, httperrors.BadRequest(err)) return diff --git a/cmd/metal-api/internal/service/image-service_test.go b/cmd/metal-api/internal/service/image-service_test.go index 54a2d7a1..5a2ef942 100644 --- a/cmd/metal-api/internal/service/image-service_test.go +++ b/cmd/metal-api/internal/service/image-service_test.go @@ -108,6 +108,48 @@ func TestDeleteImage(t *testing.T) { require.Equal(t, testdata.Img3.Name, *result.Name) } +func TestCheckImageURL(t *testing.T) { + t.Run("Invalid URL", func(t *testing.T) { + err := checkImageURL("testID", "http://invalid url", nil) + require.EqualError(t, err, "image:http://invalid url could not be parsed. error:parse \"http://invalid url\": invalid character \" \" in host name") + }) + + t.Run("HTTP URL with successful HEAD request", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + err := checkImageURL("testID", server.URL, nil) + require.NoError(t, err) + }) + + t.Run("HTTP URL with unsuccessful HEAD request", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + err := checkImageURL("testID", server.URL, nil) + require.EqualError(t, err, "image:testID is not accessible under:"+server.URL+" status:404 Not Found") + }) + + t.Run("Unsupported scheme", func(t *testing.T) { + err := checkImageURL("testID", "ftp://unsupported.url", nil) + require.EqualError(t, err, "image:testID with url:ftp://unsupported.url has unkown protocol") + }) + + t.Run("valid OCI URL", func(t *testing.T) { + err := checkImageURL("testID", "oci://ghcr.io/metal-stack/debian:latest", nil) + require.NoError(t, err) + }) + + t.Run("OCI URL with invalid reference", func(t *testing.T) { + err := checkImageURL("testID", "oci://inva lid", nil) + require.EqualError(t, err, "image reference:oci://inva lid could not be parsed. error:could not parse reference: inva lid") + }) +} + func TestCreateImage(t *testing.T) { ds, mock := datastore.InitMockDB(t) testdata.InitMockDBData(mock) diff --git a/cmd/metal-api/internal/service/partition-service.go b/cmd/metal-api/internal/service/partition-service.go index 9eee4531..54ea9a96 100644 --- a/cmd/metal-api/internal/service/partition-service.go +++ b/cmd/metal-api/internal/service/partition-service.go @@ -175,7 +175,7 @@ func (r *partitionResource) createPartition(request *restful.Request, response * imageURL = *requestPayload.PartitionBootConfiguration.ImageURL } - err = checkImageURL("image", imageURL) + err = checkImageURL("image", imageURL, nil) if err != nil { r.sendError(request, response, httperrors.BadRequest(err)) return @@ -186,7 +186,7 @@ func (r *partitionResource) createPartition(request *restful.Request, response * kernelURL = *requestPayload.PartitionBootConfiguration.KernelURL } - err = checkImageURL("kernel", kernelURL) + err = checkImageURL("kernel", kernelURL, nil) if err != nil { r.sendError(request, response, httperrors.BadRequest(err)) return @@ -275,7 +275,10 @@ func (r *partitionResource) deletePartition(request *restful.Request, response * } func (r *partitionResource) updatePartition(request *restful.Request, response *restful.Response) { - var requestPayload v1.PartitionUpdateRequest + var ( + requestPayload v1.PartitionUpdateRequest + ) + err := request.ReadEntity(&requestPayload) if err != nil { r.sendError(request, response, httperrors.BadRequest(err)) @@ -303,7 +306,7 @@ func (r *partitionResource) updatePartition(request *restful.Request, response * newPartition.Labels = requestPayload.Labels } if requestPayload.PartitionBootConfiguration.ImageURL != nil { - err = checkImageURL("image", *requestPayload.PartitionBootConfiguration.ImageURL) + err = checkImageURL("image", *requestPayload.PartitionBootConfiguration.ImageURL, nil) if err != nil { r.sendError(request, response, httperrors.BadRequest(err)) return @@ -313,11 +316,12 @@ func (r *partitionResource) updatePartition(request *restful.Request, response * } if requestPayload.PartitionBootConfiguration.KernelURL != nil { - err = checkImageURL("kernel", *requestPayload.PartitionBootConfiguration.KernelURL) + err = checkImageURL("kernel", *requestPayload.PartitionBootConfiguration.KernelURL, nil) if err != nil { r.sendError(request, response, httperrors.BadRequest(err)) return } + newPartition.BootConfiguration.KernelURL = *requestPayload.PartitionBootConfiguration.KernelURL } if requestPayload.PartitionBootConfiguration.CommandLine != nil { diff --git a/go.mod b/go.mod index 99411d6f..3027d192 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 github.com/go-openapi/spec v0.22.1 github.com/google/go-cmp v0.7.0 + github.com/google/go-containerregistry v0.20.7 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 @@ -55,6 +56,7 @@ require ( github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect @@ -63,7 +65,10 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v29.0.3+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect @@ -123,6 +128,7 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/minlz v1.0.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect @@ -165,6 +171,7 @@ require ( github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/numcpus v0.10.0 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect diff --git a/go.sum b/go.sum index d35024db..3493159f 100644 --- a/go.sum +++ b/go.sum @@ -48,6 +48,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= +github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= @@ -71,8 +73,14 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E= +github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -180,6 +188,8 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= +github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25 h1:sEDPKUw6iPjczdu33njxFjO6tYa9bfc0z/QyB/zSsBw= github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= @@ -300,6 +310,8 @@ github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= @@ -428,6 +440,8 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=