feat(jar): add OCI registry pull for JAR artifacts with backward compat
- 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
This commit is contained in:
83
internal/crd/auth.go
Normal file
83
internal/crd/auth.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package crd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
type dockerConfigJSON struct {
|
||||
Auths map[string]authn.AuthConfig `json:"auths"`
|
||||
}
|
||||
|
||||
// ResolveAuth returns an OCI authenticator for the given registry.
|
||||
// It checks in order: pull secret from K8s, then environment variables.
|
||||
// Returns authn.Anonymous if no credentials are found.
|
||||
func (crd *Crd) ResolveAuth(pullSecretName string, server string) (authn.Authenticator, error) {
|
||||
if pullSecretName != "" {
|
||||
return crd.authFromPullSecret(pullSecretName, server)
|
||||
}
|
||||
return authFromEnv(server), nil
|
||||
}
|
||||
|
||||
func (crd *Crd) authFromPullSecret(secretName string, server string) (authn.Authenticator, error) {
|
||||
ns := os.Getenv("NAMESPACE")
|
||||
secret := &corev1.Secret{}
|
||||
err := crd.runtimeClient.Get(context.Background(), types.NamespacedName{
|
||||
Namespace: ns,
|
||||
Name: secretName,
|
||||
}, secret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading pull secret %q: %w", secretName, err)
|
||||
}
|
||||
|
||||
if secret.Type != corev1.SecretTypeDockerConfigJson {
|
||||
return nil, fmt.Errorf("secret %q is not of type kubernetes.io/dockerconfigjson", secretName)
|
||||
}
|
||||
|
||||
dockerConfigJSONBytes, ok := secret.Data[corev1.DockerConfigJsonKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("secret %q missing .dockerconfigjson key", secretName)
|
||||
}
|
||||
|
||||
var cfg dockerConfigJSON
|
||||
if err := json.Unmarshal(dockerConfigJSONBytes, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing docker config from secret %q: %w", secretName, err)
|
||||
}
|
||||
|
||||
if server != "" {
|
||||
if entry, ok := cfg.Auths[server]; ok {
|
||||
return authn.FromConfig(entry), nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range cfg.Auths {
|
||||
return authn.FromConfig(entry), nil
|
||||
}
|
||||
|
||||
return authn.Anonymous, nil
|
||||
}
|
||||
|
||||
func authFromEnv(server string) authn.Authenticator {
|
||||
username := os.Getenv("REGISTRY_USERNAME")
|
||||
password := os.Getenv("REGISTRY_PASSWORD")
|
||||
envServer := os.Getenv("REGISTRY_SERVER")
|
||||
|
||||
if username == "" || password == "" {
|
||||
return authn.Anonymous
|
||||
}
|
||||
|
||||
if envServer != "" && server != "" && envServer != server {
|
||||
return authn.Anonymous
|
||||
}
|
||||
|
||||
return authn.FromConfig(authn.AuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
})
|
||||
}
|
||||
@@ -60,16 +60,22 @@ func (a Args) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
type FlinkJobSpec struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
FlinkCluster string `json:"flinkCluster"`
|
||||
Parallelism int `json:"parallelism"`
|
||||
JarURI string `json:"jarUri"`
|
||||
JarURIBasicAuthUsername *string `json:"jarURIBasicAuthUsername"`
|
||||
JarURIBasicAuthPassword *string `json:"jarURIBasicAuthPassword"`
|
||||
SavepointInterval metaV1.Duration `json:"savepointInterval"`
|
||||
EntryClass string `json:"entryClass"`
|
||||
Args Args `json:"args,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
FlinkCluster string `json:"flinkCluster"`
|
||||
Parallelism int `json:"parallelism"`
|
||||
JarRef string `json:"jarRef,omitempty"`
|
||||
JarPullSecret string `json:"jarPullSecret,omitempty"`
|
||||
SavepointInterval metaV1.Duration `json:"savepointInterval"`
|
||||
EntryClass string `json:"entryClass"`
|
||||
Args Args `json:"args,omitempty"`
|
||||
|
||||
// Deprecated: use JarRef instead. Kept for backward compatibility.
|
||||
JarURI string `json:"jarUri,omitempty"`
|
||||
// Deprecated: use JarPullSecret instead. Kept for backward compatibility.
|
||||
JarURIBasicAuthUsername *string `json:"jarURIBasicAuthUsername,omitempty"`
|
||||
// Deprecated: use JarPullSecret instead. Kept for backward compatibility.
|
||||
JarURIBasicAuthPassword *string `json:"jarURIBasicAuthPassword,omitempty"`
|
||||
}
|
||||
|
||||
type FlinkJobStatus struct {
|
||||
@@ -85,7 +91,10 @@ type FlinkJobStatus struct {
|
||||
LastRestoredSavepointDate *time.Time `json:"lastRestoredSavepointDate,omitempty"`
|
||||
LastRestoredSavepointRestoredDate *time.Time `json:"lastRestoredSavepointRestoredDate,omitempty"`
|
||||
RestoredCount int `json:"restoredCount,omitempty"`
|
||||
RunningJarURI *string `json:"runningJarURI"`
|
||||
RunningJarDigest *string `json:"runningJarDigest,omitempty"`
|
||||
RunningJarRef *string `json:"runningJarRef,omitempty"`
|
||||
// Deprecated: kept for backward compatibility during migration.
|
||||
RunningJarURI *string `json:"runningJarURI,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
@@ -140,3 +149,48 @@ const (
|
||||
LifeCycleStatusHealthy LifeCycleStatus = "HEALTHY"
|
||||
LifeCycleStatusFailed LifeCycleStatus = "FAILED"
|
||||
)
|
||||
|
||||
// EffectiveJarRef returns the OCI reference, preferring JarRef over the deprecated JarURI.
|
||||
func (s *FlinkJobSpec) EffectiveJarRef() string {
|
||||
if s.JarRef != "" {
|
||||
return s.JarRef
|
||||
}
|
||||
return s.JarURI
|
||||
}
|
||||
|
||||
// IsOCISource returns true if the job uses an OCI reference (jarRef) rather than an HTTP URL (jarUri).
|
||||
func (s *FlinkJobSpec) IsOCISource() bool {
|
||||
return s.JarRef != ""
|
||||
}
|
||||
|
||||
// RunningRef returns the currently tracked running jar reference,
|
||||
// preferring RunningJarRef (OCI) over the deprecated RunningJarURI.
|
||||
func (st *FlinkJobStatus) RunningRef() string {
|
||||
if st.RunningJarRef != nil {
|
||||
return *st.RunningJarRef
|
||||
}
|
||||
if st.RunningJarURI != nil {
|
||||
return *st.RunningJarURI
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// NeedsUpgrade returns true if the spec's jar reference differs from
|
||||
// what is currently running, indicating an upgrade is needed.
|
||||
func (s *FlinkJobSpec) NeedsUpgrade(st FlinkJobStatus) bool {
|
||||
running := st.RunningRef()
|
||||
if running == "" {
|
||||
return false
|
||||
}
|
||||
return s.EffectiveJarRef() != running
|
||||
}
|
||||
|
||||
// RunningRefPatchKey returns the status patch key and value for tracking
|
||||
// which jar reference is currently running.
|
||||
func (s *FlinkJobSpec) RunningRefPatchData() (key string, value interface{}) {
|
||||
if s.IsOCISource() {
|
||||
ref := s.EffectiveJarRef()
|
||||
return "runningJarRef", &ref
|
||||
}
|
||||
return "runningJarURI", s.JarURI
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -13,7 +15,8 @@ func (in *FlinkJob) DeepCopyInto(out *FlinkJob) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlinkJob.
|
||||
@@ -34,6 +37,111 @@ func (in *FlinkJob) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FlinkJobSpec) DeepCopyInto(out *FlinkJobSpec) {
|
||||
*out = *in
|
||||
if in.JarURIBasicAuthUsername != nil {
|
||||
in, out := &in.JarURIBasicAuthUsername, &out.JarURIBasicAuthUsername
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.JarURIBasicAuthPassword != nil {
|
||||
in, out := &in.JarURIBasicAuthPassword, &out.JarURIBasicAuthPassword
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.Args != nil {
|
||||
in, out := &in.Args, &out.Args
|
||||
*out = make(Args, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlinkJobSpec.
|
||||
func (in *FlinkJobSpec) DeepCopy() *FlinkJobSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FlinkJobSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FlinkJobStatus) DeepCopyInto(out *FlinkJobStatus) {
|
||||
*out = *in
|
||||
if in.LastSavepointPath != nil {
|
||||
in, out := &in.LastSavepointPath, &out.LastSavepointPath
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.JarId != nil {
|
||||
in, out := &in.JarId, &out.JarId
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.JobId != nil {
|
||||
in, out := &in.JobId, &out.JobId
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.Error != nil {
|
||||
in, out := &in.Error, &out.Error
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.SavepointTriggerId != nil {
|
||||
in, out := &in.SavepointTriggerId, &out.SavepointTriggerId
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.PauseSavepointTriggerId != nil {
|
||||
in, out := &in.PauseSavepointTriggerId, &out.PauseSavepointTriggerId
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.LastSavepointDate != nil {
|
||||
in, out := &in.LastSavepointDate, &out.LastSavepointDate
|
||||
*out = new(time.Time)
|
||||
**out = **in
|
||||
}
|
||||
if in.LastRestoredSavepointDate != nil {
|
||||
in, out := &in.LastRestoredSavepointDate, &out.LastRestoredSavepointDate
|
||||
*out = new(time.Time)
|
||||
**out = **in
|
||||
}
|
||||
if in.LastRestoredSavepointRestoredDate != nil {
|
||||
in, out := &in.LastRestoredSavepointRestoredDate, &out.LastRestoredSavepointRestoredDate
|
||||
*out = new(time.Time)
|
||||
**out = **in
|
||||
}
|
||||
if in.RunningJarDigest != nil {
|
||||
in, out := &in.RunningJarDigest, &out.RunningJarDigest
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.RunningJarRef != nil {
|
||||
in, out := &in.RunningJarRef, &out.RunningJarRef
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.RunningJarURI != nil {
|
||||
in, out := &in.RunningJarURI, &out.RunningJarURI
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlinkJobStatus.
|
||||
func (in *FlinkJobStatus) DeepCopy() *FlinkJobStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(FlinkJobStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *FlinkJobList) DeepCopyInto(out *FlinkJobList) {
|
||||
*out = *in
|
||||
|
||||
Reference in New Issue
Block a user