Skip to content

Commit

Permalink
Use Remote Secret for push token
Browse files Browse the repository at this point in the history
  • Loading branch information
mmorhun committed Sep 27, 2023
1 parent fe1aca0 commit cad9f8b
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 205 deletions.
9 changes: 8 additions & 1 deletion api/v1alpha1/imagerepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ type CredentialsStatus struct {

// PullSecretName is present only if ImageRepository has labels that connect it to Application and Component.
// Holds name of the dockerconfig secret with credentials to pull only from the generated repository.
// The secret is not present in the same namespace as ImageRepository, but created in
// The secret might not be present in the same namespace as ImageRepository, but created in
PullSecretName string `json:"pull-secret,omitempty"`

// PushRobotAccountName holds name of the quay robot account with write (push and pull) permissions into the generated repository.
Expand All @@ -117,6 +117,13 @@ type CredentialsStatus struct {
// PullRobotAccountName is present only if ImageRepository has labels that connect it to Application and Component.
// Holds name of the quay robot account with real (pull only) permissions from the generated repository.
PullRobotAccountName string `json:"pull-robot-account,omitempty"`

// PushRemoteSecretName holds name of RemoteSecret object that manages push Secret and its linking to pipeline Service Account.
PushRemoteSecretName string `json:"push-remote-secret,omitempty"`

// PullRemoteSecretName is present only if ImageRepository has labels that connect it to Application and Component.
// Holds the name of the RemoteSecret object that manages pull Secret.
PullRemoteSecretName string `json:"pull-remote-secret,omitempty"`
}

//+kubebuilder:object:root=true
Expand Down
14 changes: 12 additions & 2 deletions config/crd/bases/appstudio.redhat.com_imagerepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ spec:
were generated.
format: date-time
type: string
pull-remote-secret:
description: PullRemoteSecretName is present only if ImageRepository
has labels that connect it to Application and Component. Holds
the name of the RemoteSecret object that manages pull Secret.
type: string
pull-robot-account:
description: PullRobotAccountName is present only if ImageRepository
has labels that connect it to Application and Component. Holds
Expand All @@ -92,8 +97,13 @@ spec:
description: PullSecretName is present only if ImageRepository
has labels that connect it to Application and Component. Holds
name of the dockerconfig secret with credentials to pull only
from the generated repository. The secret is not present in
the same namespace as ImageRepository, but created in
from the generated repository. The secret might not be present
in the same namespace as ImageRepository, but created in
type: string
push-remote-secret:
description: PushRemoteSecretName holds name of RemoteSecret object
that manages push Secret and its linking to pipeline Service
Account.
type: string
push-robot-account:
description: PushRobotAccountName holds name of the quay robot
Expand Down
216 changes: 133 additions & 83 deletions controllers/imagerepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const (
InternalRemoteSecretLabelName = "appstudio.redhat.com/internal"

ImageRepositoryFinalizer = "appstudio.openshift.io/image-repository"

buildPipelineServiceAccountName = "appstudio-pipeline"
)

// ImageRepositoryReconciler reconciles a ImageRepository object
Expand Down Expand Up @@ -202,6 +204,10 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepository(ctx context.Context
} else {
imageRepositoryName = imageRepository.Namespace + "/" + imageRepository.Spec.Image.Name
}
imageRepository.Spec.Image.Name = imageRepositoryName

quayImageURL := fmt.Sprintf("quay.io/%s/%s", r.QuayOrganization, imageRepositoryName)
imageRepository.Status.Image.URL = quayImageURL

if imageRepository.Spec.Image.Visibility == "" {
imageRepository.Spec.Image.Visibility = imagerepositoryv1alpha1.ImageVisibilityPublic
Expand Down Expand Up @@ -233,84 +239,46 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepository(ctx context.Context
return err
}

robotAccountName := generateQuayRobotAccountName(imageRepositoryName, false)
robotAccount, err := r.QuayClient.CreateRobotAccount(r.QuayOrganization, robotAccountName)
pushCredentialsInfo, err := r.ProvisionImageRepositoryAccess(ctx, imageRepository, false)
if err != nil {
log.Error(err, "failed to create robot account", "RobotAccountName", robotAccountName, l.Action, l.ActionAdd, l.Audit, "true")
return err
}
if robotAccount == nil {
err := fmt.Errorf("unexpected response from Quay: robot account data object is nil")
log.Error(err, "nil robot account")
return err
}

err = r.QuayClient.AddPermissionsForRepositoryToRobotAccount(r.QuayOrganization, repository.Name, robotAccount.Name, true)
if err != nil {
log.Error(err, "failed to add permissions to robot account", "RobotAccountName", robotAccountName, l.Action, l.ActionUpdate, l.Audit, "true")
return err
}

quayImageURL := fmt.Sprintf("quay.io/%s/%s", r.QuayOrganization, repository.Name)
secretName := strings.ReplaceAll(robotAccountName, "_", "-")
if err := r.EnsureDockerSecret(ctx, imageRepository, robotAccount, secretName, quayImageURL); err != nil {
return err
}

status := imagerepositoryv1alpha1.ImageRepositoryStatus{}
var pullCredentialsInfo *imageRepositoryAccessData
if isComponentLinked(imageRepository) {
// Pull secret provision and propagation
pullRobotAccountName := generateQuayRobotAccountName(imageRepositoryName, true)
pullRobotAccount, err := r.QuayClient.CreateRobotAccount(r.QuayOrganization, pullRobotAccountName)
pullCredentialsInfo, err = r.ProvisionImageRepositoryAccess(ctx, imageRepository, true)
if err != nil {
log.Error(err, "failed to create pull robot account", "RobotAccountName", pullRobotAccountName, l.Action, l.ActionAdd, l.Audit, "true")
return err
}
if robotAccount == nil {
err := fmt.Errorf("unexpected response from Quay: pull robot account data object is nil")
log.Error(err, "nil pull robot account")
return err
}

err = r.QuayClient.AddPermissionsForRepositoryToRobotAccount(r.QuayOrganization, repository.Name, pullRobotAccount.Name, false)
if err != nil {
log.Error(err, "failed to add permissions to pull robot account", "RobotAccountName", robotAccountName, l.Action, l.ActionUpdate, l.Audit, "true")
return err
}

remoteSecretName := getRemoteSecretName(imageRepository)
if err := r.EnsureRemotePullSecret(ctx, imageRepository, remoteSecretName); err != nil {
return err
}

if err := r.CreateRemotePullSecretUploadSecret(ctx, pullRobotAccount, imageRepository.Namespace, remoteSecretName, quayImageURL); err != nil {
return err
}

status.Credentials.PullRobotAccountName = pullRobotAccountName
status.Credentials.PullSecretName = remoteSecretName
}

status := imagerepositoryv1alpha1.ImageRepositoryStatus{}
status.State = imagerepositoryv1alpha1.ImageRepositoryStateReady
status.Image.URL = quayImageURL
status.Image.Visibility = imageRepository.Spec.Image.Visibility
status.Credentials.PushRobotAccountName = robotAccountName
status.Credentials.PushSecretName = secretName
status.Credentials.GenerationTimestamp = &metav1.Time{Time: time.Now()}
status.Credentials.PushRobotAccountName = pushCredentialsInfo.RobotAccountName
status.Credentials.PushRemoteSecretName = pushCredentialsInfo.RemoteSecretName
status.Credentials.PushSecretName = pushCredentialsInfo.SecretName
if isComponentLinked(imageRepository) {
status.Credentials.PullRobotAccountName = pullCredentialsInfo.RobotAccountName
status.Credentials.PullRemoteSecretName = pullCredentialsInfo.RemoteSecretName
status.Credentials.PullSecretName = pullCredentialsInfo.SecretName
}

imageRepository.Spec.Image.Name = imageRepositoryName
controllerutil.AddFinalizer(imageRepository, ImageRepositoryFinalizer)
if isComponentLinked(imageRepository) {
if err := controllerutil.SetOwnerReference(component, imageRepository, r.Scheme); err != nil {
log.Error(err, "failed to set component as owner", "ComponentName", component.Name)
// Do not brake provision because of faile owner reference
// Do not brake provision because of failed owner reference
}
}
if err := r.Client.Update(ctx, imageRepository); err != nil {
log.Error(err, "failed to update CR after provision")
return err
} else {
log.Info("provisioned image repository and added finalizer")
log.Info("Finished provision of image repository and added finalizer")
}

imageRepository.Status = status
Expand All @@ -322,37 +290,68 @@ func (r *ImageRepositoryReconciler) ProvisionImageRepository(ctx context.Context
return nil
}

// RegenerateImageRepositoryCredentials rotates robot account(s) token and updates corresponding secret(s)
func (r *ImageRepositoryReconciler) RegenerateImageRepositoryCredentials(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository) error {
log := ctrllog.FromContext(ctx)
type imageRepositoryAccessData struct {
RobotAccountName string
RemoteSecretName string
SecretName string
}

// ProvisionImageRepositoryAccess makes existing quay image repository accessible
// by creating robot account and storing its token in a RemoteSecret.
func (r *ImageRepositoryReconciler) ProvisionImageRepositoryAccess(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository, isPullOnly bool) (*imageRepositoryAccessData, error) {
log := ctrllog.FromContext(ctx).WithName("ProvisionImageRepositoryAccess").WithValues("IsPullOnly", isPullOnly)
ctx = ctrllog.IntoContext(ctx, log)

imageRepositoryName := imageRepository.Spec.Image.Name
quayImageURL := imageRepository.Status.Image.URL
robotAccountName := imageRepository.Status.Credentials.PushRobotAccountName

robotAccount, err := r.QuayClient.RegenerateRobotAccountToken(r.QuayOrganization, robotAccountName)
robotAccountName := generateQuayRobotAccountName(imageRepositoryName, isPullOnly)
robotAccount, err := r.QuayClient.CreateRobotAccount(r.QuayOrganization, robotAccountName)
if err != nil {
log.Error(err, "failed to refresh push token")
return err
log.Error(err, "failed to create robot account", "RobotAccountName", robotAccountName, l.Action, l.ActionAdd, l.Audit, "true")
return nil, err
}
secretName := strings.ReplaceAll(robotAccountName, "_", "-")
if err := r.EnsureDockerSecret(ctx, imageRepository, robotAccount, secretName, quayImageURL); err != nil {
if robotAccount == nil {
err := fmt.Errorf("unexpected response from Quay: robot account data object is nil")
log.Error(err, "nil robot account")
return nil, err
}

err = r.QuayClient.AddPermissionsForRepositoryToRobotAccount(r.QuayOrganization, imageRepositoryName, robotAccount.Name, !isPullOnly)
if err != nil {
log.Error(err, "failed to add permissions to robot account", "RobotAccountName", robotAccountName, l.Action, l.ActionUpdate, l.Audit, "true")
return nil, err
}

remoteSecretName := getRemoteSecretName(imageRepository, isPullOnly)
if err := r.EnsureRemoteSecret(ctx, imageRepository, remoteSecretName, isPullOnly); err != nil {
return nil, err
}

if err := r.CreateRemoteSecretUploadSecret(ctx, robotAccount, imageRepository.Namespace, remoteSecretName, quayImageURL); err != nil {
return nil, err
}

data := &imageRepositoryAccessData{
RobotAccountName: robotAccountName,
RemoteSecretName: remoteSecretName,
SecretName: remoteSecretName,
}
return data, nil
}

// RegenerateImageRepositoryCredentials rotates robot account(s) token and updates corresponding secret(s)
func (r *ImageRepositoryReconciler) RegenerateImageRepositoryCredentials(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository) error {
log := ctrllog.FromContext(ctx)

if err := r.RegenerateImageRepositoryAccessToken(ctx, imageRepository, false); err != nil {
return err
}
log.Info("Regenerated push token", "RobotAccountName", robotAccountName)

if isComponentLinked(imageRepository) {
pullRobotAccountName := imageRepository.Status.Credentials.PullRobotAccountName
pullRobotAccount, err := r.QuayClient.RegenerateRobotAccountToken(r.QuayOrganization, pullRobotAccountName)
if err != nil {
log.Error(err, "failed to refresh pull token")
if err := r.RegenerateImageRepositoryAccessToken(ctx, imageRepository, true); err != nil {
return err
}

remoteSecretName := getRemoteSecretName(imageRepository)
if err := r.CreateRemotePullSecretUploadSecret(ctx, pullRobotAccount, imageRepository.Namespace, remoteSecretName, quayImageURL); err != nil {
return err
}
log.Info("Regenerated pull token", "RobotAccountName", pullRobotAccountName)
}

imageRepository.Spec.Credentials.RegenerateToken = nil
Expand All @@ -370,6 +369,39 @@ func (r *ImageRepositoryReconciler) RegenerateImageRepositoryCredentials(ctx con
return nil
}

// RegenerateImageRepositoryAccessToken rotates robot account token and updates new one to the corresponding Remote Secret.
func (r *ImageRepositoryReconciler) RegenerateImageRepositoryAccessToken(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository, isPullOnly bool) error {
log := ctrllog.FromContext(ctx).WithName("RegenerateImageRepositoryAccessToken").WithValues("IsPullOnly", isPullOnly)
ctx = ctrllog.IntoContext(ctx, log)

quayImageURL := imageRepository.Status.Image.URL

robotAccountName := imageRepository.Status.Credentials.PushRobotAccountName
if isPullOnly {
robotAccountName = imageRepository.Status.Credentials.PullRobotAccountName
}
robotAccount, err := r.QuayClient.RegenerateRobotAccountToken(r.QuayOrganization, robotAccountName)
if err != nil {
log.Error(err, "failed to refresh robot account token")
return err
} else {
log.Info("Refreshed quay robot account token")
}

remoteSecretName := imageRepository.Status.Credentials.PushRemoteSecretName
if isPullOnly {
remoteSecretName = imageRepository.Status.Credentials.PullRemoteSecretName
}
if err := r.EnsureRemoteSecret(ctx, imageRepository, remoteSecretName, isPullOnly); err != nil {
return err
}
if err := r.CreateRemoteSecretUploadSecret(ctx, robotAccount, imageRepository.Namespace, remoteSecretName, quayImageURL); err != nil {
return err
}

return nil
}

// CleanupImageRepository deletes image repository and corresponding robot account(s).
func (r *ImageRepositoryReconciler) CleanupImageRepository(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository) {
log := ctrllog.FromContext(ctx).WithName("RepositoryCleanup")
Expand Down Expand Up @@ -489,7 +521,7 @@ func (r *ImageRepositoryReconciler) EnsureDockerSecret(ctx context.Context, imag
return nil
}

func (r *ImageRepositoryReconciler) EnsureRemotePullSecret(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository, remoteSecretName string) error {
func (r *ImageRepositoryReconciler) EnsureRemoteSecret(ctx context.Context, imageRepository *imagerepositoryv1alpha1.ImageRepository, remoteSecretName string, isPull bool) error {
log := ctrllog.FromContext(ctx).WithValues("RemoteSecretName", remoteSecretName)

remoteSecret := &remotesecretv1beta1.RemoteSecret{}
Expand All @@ -500,13 +532,16 @@ func (r *ImageRepositoryReconciler) EnsureRemotePullSecret(ctx context.Context,
return err
}

serviceAccountName := buildPipelineServiceAccountName
if isPull {
serviceAccountName = defaultServiceAccountName
}

remoteSecret := &remotesecretv1beta1.RemoteSecret{
ObjectMeta: metav1.ObjectMeta{
Name: remoteSecretName,
Namespace: imageRepository.Namespace,
Labels: map[string]string{
ApplicationNameLabelName: imageRepository.Labels[ApplicationNameLabelName],
ComponentNameLabelName: imageRepository.Labels[ComponentNameLabelName],
InternalRemoteSecretLabelName: "true",
},
},
Expand All @@ -518,14 +553,22 @@ func (r *ImageRepositoryReconciler) EnsureRemotePullSecret(ctx context.Context,
{
ServiceAccount: remotesecretv1beta1.ServiceAccountLink{
Reference: corev1.LocalObjectReference{
Name: defaultServiceAccountName,
Name: serviceAccountName,
},
},
},
},
},
},
}

if isPull {
remoteSecret.Labels[ApplicationNameLabelName] = imageRepository.Labels[ApplicationNameLabelName]
remoteSecret.Labels[ComponentNameLabelName] = imageRepository.Labels[ComponentNameLabelName]
} else {
remoteSecret.Spec.Targets = []remotesecretv1beta1.RemoteSecretTarget{{Namespace: imageRepository.Namespace}}
}

if err := controllerutil.SetOwnerReference(imageRepository, remoteSecret, r.Scheme); err != nil {
log.Error(err, "failed to set owner for remote secret")
return err
Expand All @@ -534,14 +577,16 @@ func (r *ImageRepositoryReconciler) EnsureRemotePullSecret(ctx context.Context,
if err := r.Client.Create(ctx, remoteSecret); err != nil {
log.Error(err, "failed to create remote secret", l.Action, l.ActionAdd, l.Audit, "true")
return err
} else {
log.Info("Remote Secret created")
}
}

return nil
}

// CreateRemotePullSecretUploadSecret propagates credentials from given robot account to corresponding remote secret.
func (r *ImageRepositoryReconciler) CreateRemotePullSecretUploadSecret(ctx context.Context, robotAccount *quay.RobotAccount, namespace, remoteSecretName, imageURL string) error {
// CreateRemoteSecretUploadSecret propagates credentials from given robot account to corresponding remote secret.
func (r *ImageRepositoryReconciler) CreateRemoteSecretUploadSecret(ctx context.Context, robotAccount *quay.RobotAccount, namespace, remoteSecretName, imageURL string) error {
uploadSecretName := "upload-secret-" + remoteSecretName
log := ctrllog.FromContext(ctx).WithValues("RemoteSecretName", remoteSecretName).WithValues("UploadSecretName", uploadSecretName)

Expand All @@ -562,6 +607,8 @@ func (r *ImageRepositoryReconciler) CreateRemotePullSecretUploadSecret(ctx conte
if err := r.Client.Create(ctx, uploadSecret); err != nil {
log.Error(err, "failed to create upload secret", l.Action, l.ActionAdd, l.Audit, "true")
return err
} else {
log.Info("Created upload Secret for Remote Secret")
}

return nil
Expand All @@ -588,12 +635,15 @@ func generateQuayRobotAccountName(imageRepositoryName string, isPullOnly bool) s
return robotAccountName
}

func getRemoteSecretName(imageRepository *imagerepositoryv1alpha1.ImageRepository) string {
componentName := imageRepository.Labels[ComponentNameLabelName]
if len(componentName) > 220 {
componentName = componentName[:220]
func getRemoteSecretName(imageRepository *imagerepositoryv1alpha1.ImageRepository, isPullOnly bool) string {
remoteSecretName := imageRepository.Name
if len(remoteSecretName) > 220 {
remoteSecretName = remoteSecretName[:220]
}
if isPullOnly {
remoteSecretName += "-image-pull"
}
return componentName + "-image-pull"
return remoteSecretName
}

func isComponentLinked(imageRepository *imagerepositoryv1alpha1.ImageRepository) bool {
Expand Down
Loading

0 comments on commit cad9f8b

Please sign in to comment.