github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/pkg/wasm/client.go (about) 1 // Copyright 2022 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package wasm 16 17 import ( 18 "archive/tar" 19 "bytes" 20 "compress/gzip" 21 "context" 22 "errors" 23 "fmt" 24 "io" 25 "io/fs" 26 "net/http" 27 "os" 28 "path/filepath" 29 "time" 30 31 "github.com/google/go-containerregistry/pkg/v1/match" 32 33 "github.com/GoogleContainerTools/kpt/pkg/oci" 34 "github.com/google/go-containerregistry/pkg/gcrane" 35 "github.com/google/go-containerregistry/pkg/name" 36 v1 "github.com/google/go-containerregistry/pkg/v1" 37 "github.com/google/go-containerregistry/pkg/v1/empty" 38 "github.com/google/go-containerregistry/pkg/v1/mutate" 39 "github.com/google/go-containerregistry/pkg/v1/remote" 40 "github.com/google/go-containerregistry/pkg/v1/remote/transport" 41 "github.com/google/go-containerregistry/pkg/v1/stream" 42 "github.com/google/go-containerregistry/pkg/v1/types" 43 ) 44 45 type Client struct { 46 *oci.Storage 47 } 48 49 func NewClient(cacheDir string) (*Client, error) { 50 store, err := oci.NewStorage(cacheDir) 51 if err != nil { 52 return nil, err 53 } 54 return &Client{Storage: store}, nil 55 } 56 57 func (r *Client) PushWasm(ctx context.Context, wasmFile string, imageName string) error { 58 tag, err := name.NewTag(imageName) 59 if err != nil { 60 return fmt.Errorf("unable to parse tag %q: %v", imageName, err) 61 } 62 63 options := []remote.Option{ 64 remote.WithAuthFromKeychain(gcrane.Keychain), 65 remote.WithContext(ctx), 66 } 67 68 rmt, err := remote.Get(tag, options...) 69 if err != nil && !isManifestNotFoundErr(err) { 70 return fmt.Errorf("unexpected error when trying to check the remote registry: %w", err) 71 } 72 73 // If there is an existing remote image, we ensure its media type is an image index type. 74 // TODO: if the media type is not an image index type, we can do something smarter than 75 // simply failing. 76 if err == nil { 77 if rmt.MediaType != types.OCIImageIndex && rmt.MediaType != types.DockerManifestList { 78 return fmt.Errorf("unexpected media type: %s, expect either %s or %s", rmt.MediaType, types.OCIImageIndex, types.DockerManifestList) 79 } 80 } 81 82 // Compress the wasm file. 83 tarReader, err := wasmFileToTar(wasmFile) 84 if err != nil { 85 return fmt.Errorf("unable to compress %v: %w", wasmFile, err) 86 } 87 88 // Construct the image by building its layer. 89 layer := stream.NewLayer(io.NopCloser(tarReader), stream.WithCompressionLevel(gzip.BestCompression)) 90 if err = remote.WriteLayer(tag.Repository, layer, options...); err != nil { 91 return fmt.Errorf("failed to write remote layer: %w", err) 92 } 93 img, err := mutate.AppendLayers(empty.Image, layer) 94 if err != nil { 95 return fmt.Errorf("failed to append image layers: %w", err) 96 } 97 img = mutate.MediaType(img, types.DockerManifestSchema2) 98 img, err = mutate.CreatedAt(img, v1.Time{Time: time.Now()}) 99 if err != nil { 100 return fmt.Errorf("failed to set created time for the image: %w", err) 101 } 102 hash, err := img.Digest() 103 if err != nil { 104 return fmt.Errorf("failed to get digest of the image: %w", err) 105 } 106 107 // Push the image to remote 108 if err = remote.Write(tag.Repository.Digest(hash.String()), img, options...); err != nil { 109 return fmt.Errorf("failed to push image %s: %w", tag, err) 110 } 111 fmt.Printf("digest of the image: %v\n", hash.String()) 112 113 // Construct the image index. 114 var index v1.ImageIndex = empty.Index 115 if rmt != nil { 116 index, err = rmt.ImageIndex() 117 if err != nil { 118 return fmt.Errorf("unable to parse an image as image index: %w", err) 119 } 120 } 121 122 wasmPlatform := v1.Platform{ 123 Architecture: "wasm", 124 OS: "js", 125 } 126 index = mutate.RemoveManifests(index, match.Platforms(wasmPlatform)) 127 index = mutate.AppendManifests(index, mutate.IndexAddendum{ 128 Add: img, 129 Descriptor: v1.Descriptor{ 130 MediaType: "application/vnd.docker.distribution.manifest.v2+json", 131 Digest: hash, 132 Platform: &wasmPlatform, 133 }, 134 }) 135 index = mutate.IndexMediaType(index, types.DockerManifestList) 136 137 // Push the image index to remote. 138 err = remote.WriteIndex(tag, index, options...) 139 if err != nil { 140 return fmt.Errorf("unable to write image index: %w", err) 141 } 142 indexHash, err := index.Digest() 143 if err != nil { 144 return fmt.Errorf("unable to get digest for the image index: %w", err) 145 } 146 fmt.Printf("digest of the image index: %v\n", indexHash.String()) 147 return nil 148 } 149 150 func wasmFileToTar(filename string) (io.Reader, error) { 151 buf := bytes.NewBuffer(nil) 152 writer := tar.NewWriter(buf) 153 154 b, err := os.ReadFile(filename) 155 if err != nil { 156 return nil, fmt.Errorf("failed to read from file %v: %w", filename, err) 157 } 158 blen := len(b) 159 160 if err = writer.WriteHeader(&tar.Header{ 161 Name: filepath.Base(filename), 162 Size: int64(blen), 163 Mode: 0644, 164 }); err != nil { 165 return nil, fmt.Errorf("failed to write tar header: %w", err) 166 } 167 168 if _, err = writer.Write(b); err != nil { 169 return nil, fmt.Errorf("failed to write tar contents: %w", err) 170 } 171 return buf, nil 172 } 173 174 func (r *Client) LoadWasm(ctx context.Context, imageName string) (io.ReadCloser, error) { 175 ref, err := name.ParseReference(imageName) 176 if err != nil { 177 return nil, err 178 } 179 180 fetcher := func() (io.ReadCloser, error) { 181 options := []remote.Option{ 182 remote.WithContext(ctx), 183 remote.WithAuthFromKeychain(gcrane.Keychain), 184 remote.WithPlatform(v1.Platform{ 185 Architecture: "wasm", 186 OS: "js", 187 }), 188 } 189 ociImage, err := remote.Image(ref, options...) 190 if err != nil { 191 return nil, fmt.Errorf("unable to get remote image: %w", err) 192 } 193 194 reader := mutate.Extract(ociImage) 195 return reader, nil 196 } 197 198 // We need the per-digest cache here because otherwise we have to make a network request to look up the manifest in remote.Image 199 // (this could be cached by the go-containerregistry library, for some reason it is not...) 200 // TODO: Is there then any real reason to _also_ have the image-layer cache? 201 f, err := oci.WithCacheFile(filepath.Join(r.GetCacheDir(), "wasm", ref.String()), fetcher) 202 if err != nil { 203 return nil, err 204 } 205 206 wrapper := &tarReadCloser{ 207 Reader: tar.NewReader(f), 208 closer: f, 209 } 210 211 wasmBytesReader, err := loadWasmFromTar(wrapper) 212 if err != nil { 213 return nil, err 214 } 215 return wasmBytesReader, nil 216 } 217 218 type tarReadCloser struct { 219 *tar.Reader 220 closer io.Closer 221 } 222 223 func (w *tarReadCloser) Close() error { 224 return w.closer.Close() 225 } 226 227 func loadWasmFromTar(tarReadCloser *tarReadCloser) (io.ReadCloser, error) { 228 for { 229 hdr, err := tarReadCloser.Next() 230 if err == io.EOF { 231 break 232 } 233 if err != nil { 234 return nil, err 235 } 236 path := hdr.Name 237 fileType := hdr.FileInfo().Mode().Type() 238 switch fileType { 239 case fs.ModeDir: 240 // Ignored 241 case fs.ModeSymlink: 242 // We probably don't want to support this; feels high-risk, low-reward 243 return nil, fmt.Errorf("package cannot contain symlink (%q)", path) 244 case 0: 245 // there is only one file in it 246 return tarReadCloser, nil 247 default: 248 return nil, fmt.Errorf("package cannot unsupported entry type for %q (%v)", path, fileType) 249 } 250 } 251 252 return nil, nil 253 } 254 255 func isManifestNotFoundErr(err error) bool { 256 if err == nil { 257 return false 258 } 259 var terr *transport.Error 260 isTransportError := errors.As(err, &terr) 261 if !isTransportError { 262 return false 263 } 264 if terr.StatusCode != http.StatusNotFound { 265 return false 266 } 267 foundManifestUnknown := false 268 for _, e := range terr.Errors { 269 if e.Code == transport.ManifestUnknownErrorCode { 270 foundManifestUnknown = true 271 } 272 } 273 return foundManifestUnknown 274 }