package jar import ( "crypto/rand" "encoding/hex" "errors" "fmt" "io" "net/http" "os" "strings" "flink-kube-operator/pkg" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/types" api "github.com/logi-camp/go-flink-client" "go.uber.org/zap" ) type JarFile struct { ref string filePath string digest string basicAuthUsername *string basicAuthPassword *string auth authn.Authenticator } // NewJarFile creates a JarFile from an OCI reference, pulling the artifact. // auth is the OCI authenticator (from pull secret or env vars). Use authn.Anonymous for public registries. func NewJarFile(ref string, auth authn.Authenticator) (*JarFile, error) { jarFile := &JarFile{ ref: ref, auth: auth, } err := jarFile.Pull() if err != nil { return nil, err } return jarFile, nil } // NewJarFileFromURI creates a JarFile from an HTTP URI (deprecated path, kept for backward compat). func NewJarFileFromURI(uri string, basicAuthUsername *string, basicAuthPassword *string) (*JarFile, error) { jarFile := &JarFile{ ref: uri, basicAuthUsername: basicAuthUsername, basicAuthPassword: basicAuthPassword, } err := jarFile.Download() if err != nil { return nil, err } return jarFile, nil } func (jarFile *JarFile) Upload(flinkClient *api.Client) (fileName string, err error) { resp, err := flinkClient.UploadJar(jarFile.filePath) if err != nil { pkg.Logger.Error("[jar] error uploading jar", zap.Error(err)) return "", err } filePathParts := strings.Split(resp.FileName, "/") fileName = filePathParts[len(filePathParts)-1] if resp.Status != "success" { err = errors.New("jar upload was not success") } jarFile.delete() return } func (jarFile *JarFile) Digest() string { return jarFile.digest } func (jarFile *JarFile) Pull() error { randBytes := make([]byte, 16) rand.Read(randBytes) fileName := hex.EncodeToString(randBytes) jarFile.filePath = "/tmp/" + fileName + ".jar" ref, err := name.ParseReference(jarFile.ref) if err != nil { return fmt.Errorf("parsing OCI reference %q: %w", jarFile.ref, err) } img, err := remote.Image(ref, remote.WithAuth(jarFile.auth)) if err != nil { return fmt.Errorf("fetching OCI image %q: %w", jarFile.ref, err) } d, err := img.Digest() if err != nil { return fmt.Errorf("getting image digest: %w", err) } jarFile.digest = d.String() layers, err := img.Layers() if err != nil { return fmt.Errorf("getting image layers: %w", err) } if len(layers) == 0 { return fmt.Errorf("OCI image %q has no layers", jarFile.ref) } layer := layers[0] mediaType, err := layer.MediaType() if err != nil { return fmt.Errorf("getting layer media type: %w", err) } if mediaType != types.OCILayer && mediaType != types.OCIUncompressedLayer && mediaType != types.DockerLayer && mediaType != types.DockerUncompressedLayer && string(mediaType) != "application/java-archive" { pkg.Logger.Warn("[jar] unexpected layer media type, expected application/java-archive or standard OCI layer", zap.String("got", string(mediaType)), zap.String("ref", jarFile.ref), ) } blob, err := layer.Compressed() if err != nil { return fmt.Errorf("reading layer blob: %w", err) } defer blob.Close() out, err := os.Create(jarFile.filePath) if err != nil { return fmt.Errorf("creating temp file: %w", err) } defer out.Close() _, err = io.Copy(out, blob) if err != nil { jarFile.delete() return fmt.Errorf("writing layer to disk: %w", err) } pkg.Logger.Info("[jar] pulled OCI artifact", zap.String("ref", jarFile.ref), zap.String("digest", jarFile.digest), zap.String("path", jarFile.filePath), ) return nil } func (jarFile *JarFile) Download() error { randBytes := make([]byte, 16) rand.Read(randBytes) fileName := hex.EncodeToString(randBytes) jarFile.filePath = "/tmp/" + fileName + ".jar" out, err := os.Create(jarFile.filePath) if err != nil { return err } defer out.Close() var resp *http.Response if jarFile.basicAuthPassword != nil && jarFile.basicAuthUsername != nil { req, err := http.NewRequest("GET", jarFile.ref, nil) if err != nil { jarFile.delete() return err } req.SetBasicAuth(*jarFile.basicAuthUsername, *jarFile.basicAuthPassword) resp, err = http.DefaultClient.Do(req) } else { resp, err = http.Get(jarFile.ref) } if err != nil { jarFile.delete() pkg.Logger.Error("error in downloading jar", zap.Error(err)) return err } if resp.StatusCode > 299 { err = fmt.Errorf("download failed: status %s", resp.Status) pkg.Logger.Error("error in downloading jar", zap.Error(err)) return err } defer resp.Body.Close() _, err = io.Copy(out, resp.Body) if err != nil { return err } return nil } func (jarFile *JarFile) delete() error { pkg.Logger.Info("[jar] [delete]", zap.String("path", jarFile.filePath)) err := os.Remove(jarFile.filePath) if err != nil { pkg.Logger.Error("[jar] [delete]", zap.Error(err)) } return err }