- Add jarRef and jarPullSecret fields to FlinkJob CRD (jarUri/basicAuth deprecated) - OCI pull via go-containerregistry with dual auth (K8s pull secret + env vars) - Media type validation on pulled layers - Atomic status patches with runningJarRef/runningJarDigest tracking - NeedsUpgrade/RunningRef/RunningRefPatchData domain helpers - README with usage guide, pushing JARs, and GitHub Actions CI/CD workflow - CONTEXT.md domain glossary
198 lines
5.0 KiB
Go
198 lines
5.0 KiB
Go
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
|
|
}
|