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:
2026-07-24 17:37:30 +03:30
parent 91c10c89da
commit a36e72877d
15 changed files with 750 additions and 92 deletions

83
internal/crd/auth.go Normal file
View 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,
})
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -2,31 +2,51 @@ package jar
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"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 {
uri string
ref string
filePath string
digest string
basicAuthUsername *string
basicAuthPassword *string
auth authn.Authenticator
}
func NewJarFile(URI string, basicAuthUsername *string, basicAuthPassword *string) (*JarFile, error) {
// 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{
uri: URI,
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,
}
@@ -38,10 +58,10 @@ func NewJarFile(URI string, basicAuthUsername *string, basicAuthPassword *string
}
func (jarFile *JarFile) Upload(flinkClient *api.Client) (fileName string, err error) {
resp, err := flinkClient.UploadJar(jarFile.filePath)
if err != nil {
pkg.Logger.Error("[main] error uploading jar", zap.Error(err))
pkg.Logger.Error("[jar] error uploading jar", zap.Error(err))
return "", err
}
filePathParts := strings.Split(resp.FileName, "/")
fileName = filePathParts[len(filePathParts)-1]
@@ -52,6 +72,78 @@ func (jarFile *JarFile) Upload(flinkClient *api.Client) (fileName string, err er
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)
@@ -66,31 +158,15 @@ func (jarFile *JarFile) Download() error {
var resp *http.Response
if jarFile.basicAuthPassword != nil && jarFile.basicAuthUsername != nil {
basicAuth := func(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
redirectPolicyFunc := func(req *http.Request, via []*http.Request) error {
req.Header.Add("Authorization", "Basic "+basicAuth(*jarFile.basicAuthUsername, *jarFile.basicAuthPassword))
return nil
}
client := &http.Client{
Jar: &cookiejar.Jar{},
CheckRedirect: redirectPolicyFunc,
}
req, err := http.NewRequest("GET", jarFile.uri, nil)
req, err := http.NewRequest("GET", jarFile.ref, nil)
if err != nil {
jarFile.delete()
return err
}
req.Header.Add("Authorization", "Basic "+basicAuth(*jarFile.basicAuthUsername, *jarFile.basicAuthPassword))
resp, err = client.Do(req)
req.SetBasicAuth(*jarFile.basicAuthUsername, *jarFile.basicAuthPassword)
resp, err = http.DefaultClient.Do(req)
} else {
resp, err = http.Get(jarFile.uri)
resp, err = http.Get(jarFile.ref)
}
if err != nil {
jarFile.delete()
@@ -98,9 +174,7 @@ func (jarFile *JarFile) Download() error {
return err
}
if resp.StatusCode > 299 {
respBody := []byte{}
resp.Body.Read(respBody)
err = errors.New(string(respBody) + " status:" + resp.Status)
err = fmt.Errorf("download failed: status %s", resp.Status)
pkg.Logger.Error("error in downloading jar", zap.Error(err))
return err
}

View File

@@ -10,8 +10,6 @@ import (
)
func (job *ManagedJob) Cycle() {
// pkg.Logger.Debug("[managed-job] [new] check cycle", zap.String("jobName", job.def.GetName()))
// Init job
if job.def.Status.LifeCycleStatus == "" && (job.def.Status.JobStatus == "" || job.def.Status.JobStatus == v1alpha1.JobStatusFinished) {
job.Run(false)
@@ -26,7 +24,8 @@ func (job *ManagedJob) Cycle() {
job.trackSavepoint()
}
}
if job.def.Status.RunningJarURI != nil && job.def.Spec.JarURI != *job.def.Status.RunningJarURI {
if job.def.Spec.NeedsUpgrade(job.def.Status) {
job.upgrade()
}
@@ -42,14 +41,6 @@ func (job *ManagedJob) Cycle() {
job.crd.SetJobStatus(job.def.UID, job.def.Status)
return
}
// if job.def.Status.JobStatus == v1alpha1.JobStatusFailed && job.def.Status.LastSavepointPath != nil {
// //job.restore()
// return
// }
// if job.def.Status.JobStatus == v1alpha1.JobStatusFailed && job.def.Status.LastSavepointPath == nil {
// //job.restore()
// return
// }
pkg.Logger.Warn("[managed-job] [cycle] unhandled job status", zap.String("name", job.def.Name), zap.String("status", string(job.def.Status.JobStatus)), zap.String("namespace", job.def.Namespace))
}

View File

@@ -4,41 +4,90 @@ import (
"flink-kube-operator/internal/jar"
"flink-kube-operator/pkg"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"go.uber.org/zap"
)
// upload jar file and set the jarId for later usages
// upload pulls/downloads the jar and uploads it to Flink, then patches the CRD status.
func (job *ManagedJob) upload() error {
jarFile, err := jar.NewJarFile(job.def.Spec.JarURI, job.def.Spec.JarURIBasicAuthUsername, job.def.Spec.JarURIBasicAuthPassword)
spec := job.def.Spec
var jarFile *jar.JarFile
var err error
if spec.IsOCISource() {
auth, authErr := job.resolveAuth()
if authErr != nil {
pkg.Logger.Error("[managed-job] [upload] auth resolution failed", zap.Error(authErr))
return authErr
}
jarFile, err = jar.NewJarFile(spec.EffectiveJarRef(), auth)
} else {
jarFile, err = jar.NewJarFileFromURI(spec.JarURI, spec.JarURIBasicAuthUsername, spec.JarURIBasicAuthPassword)
}
if err != nil {
pkg.Logger.Debug("[manage-job] [upload] error on download jar", zap.Error(err))
pkg.Logger.Debug("[managed-job] [upload] error on pull/download jar", zap.Error(err))
return err
}
jarId, err := jarFile.Upload(job.client)
if err != nil {
pkg.Logger.Debug("[manage-job] [upload] error on upload jar", zap.Error(err))
pkg.Logger.Debug("[managed-job] [upload] error on upload jar", zap.Error(err))
return err
}
pkg.Logger.Info("[manage-job] [upload] uploaded", zap.Any("upload-jar-resp", jarId))
pkg.Logger.Info("[managed-job] [upload] uploaded", zap.String("jarId", jarId))
job.def.Status.JarId = &jarId
job.crd.Patch(job.def.UID, map[string]interface{}{
patchData := map[string]interface{}{
"status": map[string]interface{}{
"jarId": job.def.Status.JarId,
"jarId": &jarId,
},
})
}
if spec.IsOCISource() {
digest := jarFile.Digest()
patchData["status"].(map[string]interface{})["runningJarDigest"] = &digest
}
job.crd.Patch(job.def.UID, patchData)
return nil
}
func (job *ManagedJob) resolveAuth() (authn.Authenticator, error) {
s := job.def.Spec
server := ""
ref := s.EffectiveJarRef()
if reg, err := extractRegistry(ref); err == nil {
server = reg
}
return job.crd.ResolveAuth(s.JarPullSecret, server)
}
func extractRegistry(ref string) (string, error) {
r, err := name.NewRegistry(ref)
if err != nil {
t, err2 := name.NewTag(ref)
if err2 != nil {
return "", err
}
return t.RegistryStr(), nil
}
return r.RegistryStr(), nil
}
func (job *ManagedJob) RemoveJar() {
if job.def.Status.JarId != nil {
err := job.client.DeleteJar(*job.def.Status.JarId)
pkg.Logger.Error("[managed-job] [jar]", zap.Error(err))
if err != nil {
pkg.Logger.Error("[managed-job] [jar] delete jar error", zap.Error(err))
}
err = job.crd.Patch(job.def.UID, map[string]interface{}{
"status": map[string]interface{}{
"jarId": nil,
},
})
pkg.Logger.Error("[managed-job] [jar]", zap.Error(err))
if err != nil {
pkg.Logger.Error("[managed-job] [jar] patch jarId error", zap.Error(err))
}
}
}

View File

@@ -83,17 +83,21 @@ func (job *ManagedJob) Run(restoreMode bool) error {
// job.def.Status.JobId = &runJarResp.JobId
// job.def.Status.JobStatus = v1alpha1.JobStatusCreating
// job.def.Status.Error = nil
statusPatch := map[string]interface{}{
"jobId": jobId,
"jobStatus": v1alpha1.JobStatusCreating,
"lifeCycleStatus": v1alpha1.LifeCycleStatusRestoring,
"lastRestoredSavepointDate": job.def.Status.LastSavepointDate,
"restoredCount": job.def.Status.RestoredCount + 1,
"lastRestoredSavepointRestoredDate": time.Now().Format(time.RFC3339),
"error": nil,
}
refKey, refVal := job.def.Spec.RunningRefPatchData()
statusPatch[refKey] = refVal
job.crd.Patch(job.def.UID, map[string]interface{}{
"status": map[string]interface{}{
"jobId": jobId,
"runningJarURI": job.def.Spec.JarURI,
"jobStatus": v1alpha1.JobStatusCreating,
"lifeCycleStatus": v1alpha1.LifeCycleStatusRestoring,
"lastRestoredSavepointDate": job.def.Status.LastSavepointDate,
"restoredCount": job.def.Status.RestoredCount + 1,
"lastRestoredSavepointRestoredDate": time.Now().Format(time.RFC3339),
"error": nil,
},
"status": statusPatch,
})
return nil

View File

@@ -7,10 +7,13 @@ import (
)
func (job *ManagedJob) upgrade() {
pkg.Logger.Info("[managed-job] [upgrade] pausing... ",
effectiveRef := job.def.Spec.EffectiveJarRef()
prevRef := job.def.Status.RunningRef()
pkg.Logger.Info("[managed-job] [upgrade] pausing...",
zap.String("jobName", job.def.GetName()),
zap.String("currentJarURI", job.def.Spec.JarURI),
zap.String("prevJarURI", *job.def.Status.RunningJarURI),
zap.String("currentRef", effectiveRef),
zap.String("prevRef", prevRef),
)
job.def.Status.JarId = nil
job.crd.Patch(job.def.UID, map[string]interface{}{
@@ -23,11 +26,10 @@ func (job *ManagedJob) upgrade() {
pkg.Logger.Error("[managed-job] [upgrade] error in pausing", zap.Error(err))
return
}
pkg.Logger.Info("[managed-job] [upgrade] restoring... ",
pkg.Logger.Info("[managed-job] [upgrade] restoring...",
zap.String("jobName", job.def.GetName()),
zap.String("currentJarURI", job.def.Spec.JarURI),
zap.String("prevJarURI", *job.def.Status.RunningJarURI),
zap.Error(err),
zap.String("currentRef", effectiveRef),
zap.String("prevRef", prevRef),
)
err = job.Run(true)