diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index c449e20c..922e9cac 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -61,6 +61,24 @@ type JenkinsSpec struct { // ServiceAccount defines Jenkins master service account attributes // +optional ServiceAccount ServiceAccount `json:"serviceAccount,omitempty"` + + // JenkinsAPISettings defines configuration used by the operator to gain admin access to the Jenkins API + JenkinsAPISettings JenkinsAPISettings `json:"jenkinsAPISettings"` +} + +// AuthorizationStrategy defines authorization strategy of the operator for the Jenkins API +type AuthorizationStrategy string + +const ( + // CreateUserAuthorizationStrategy operator sets HudsonPrivateSecurityRealm and FullControlOnceLoggedInAuthorizationStrategy than creates user using init.d groovy script + CreateUserAuthorizationStrategy AuthorizationStrategy = "createUser" + // ServiceAccountAuthorizationStrategy operator gets token associated with Jenkins service account and uses it as bearer token + ServiceAccountAuthorizationStrategy AuthorizationStrategy = "serviceAccount" +) + +// JenkinsAPISettings defines configuration used by the operator to gain admin access to the Jenkins API +type JenkinsAPISettings struct { + AuthorizationStrategy AuthorizationStrategy `json:"authorizationStrategy"` } // ServiceAccount defines Kubernetes service account attributes diff --git a/pkg/controller/jenkins/client/jenkins.go b/pkg/controller/jenkins/client/jenkins.go index 27022a8c..ee2ac9e3 100644 --- a/pkg/controller/jenkins/client/jenkins.go +++ b/pkg/controller/jenkins/client/jenkins.go @@ -68,6 +68,23 @@ type JenkinsAPIConnectionSettings struct { UseNodePort bool } +type setBearerToken struct { + rt http.RoundTripper + token string +} + +func (t *setBearerToken) transport() http.RoundTripper { + if t.rt != nil { + return t.rt + } + return http.DefaultTransport +} + +func (t *setBearerToken) RoundTrip(r *http.Request) (*http.Response, error) { + r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.token)) + return t.transport().RoundTrip(r) +} + // CreateOrUpdateJob creates or updates a job from config func (jenkins *jenkins) CreateOrUpdateJob(config, jobName string) (job *gojenkins.Job, created bool, err error) { // create or update @@ -114,19 +131,37 @@ func (j JenkinsAPIConnectionSettings) Validate() error { return nil } -// New creates Jenkins API client -func New(url, user, passwordOrToken string) (Jenkins, error) { +// NewUserAndPasswordAuthorization creates Jenkins API client with user and password authorization +func NewUserAndPasswordAuthorization(url, userName, passwordOrToken string) (Jenkins, error) { + return newClient(url, userName, passwordOrToken) +} + +// NewBearerTokenAuthorization creates Jenkins API client with bearer token authorization +func NewBearerTokenAuthorization(url, token string) (Jenkins, error) { + return newClient(url, "", token) +} + +func newClient(url, userName, passwordOrToken string) (Jenkins, error) { if strings.HasSuffix(url, "/") { url = url[:len(url)-1] } jenkinsClient := &jenkins{} jenkinsClient.Server = url + + var basicAuth *gojenkins.BasicAuth + httpClient := http.DefaultClient + if len(userName) > 0 && len(passwordOrToken) > 0 { + basicAuth = &gojenkins.BasicAuth{Username: userName, Password: passwordOrToken} + } else { + httpClient.Transport = &setBearerToken{token: passwordOrToken, rt: httpClient.Transport} + } + jenkinsClient.Requester = &gojenkins.Requester{ Base: url, SslVerify: true, - Client: http.DefaultClient, - BasicAuth: &gojenkins.BasicAuth{Username: user, Password: passwordOrToken}, + Client: httpClient, + BasicAuth: basicAuth, } if _, err := jenkinsClient.Init(); err != nil { return nil, errors.Wrap(err, "couldn't init Jenkins API client") diff --git a/pkg/controller/jenkins/configuration/backuprestore/backuprestore.go b/pkg/controller/jenkins/configuration/backuprestore/backuprestore.go index 4216cab1..03b75ac5 100644 --- a/pkg/controller/jenkins/configuration/backuprestore/backuprestore.go +++ b/pkg/controller/jenkins/configuration/backuprestore/backuprestore.go @@ -1,25 +1,19 @@ package backuprestore import ( - "bytes" "context" "fmt" "time" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" jenkinsclient "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/client" + "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/configuration/base/resources" "github.com/jenkinsci/kubernetes-operator/pkg/log" "github.com/go-logr/logr" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/remotecommand" k8s "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -61,29 +55,27 @@ var triggers = backupTriggers{triggers: make(map[string]backupTrigger)} // BackupAndRestore represents Jenkins backup and restore client type BackupAndRestore struct { - config rest.Config - k8sClient k8s.Client - clientSet kubernetes.Clientset - - logger logr.Logger - jenkins *v1alpha2.Jenkins + configuration.Configuration + logger logr.Logger } // New returns Jenkins backup and restore client -func New(k8sClient k8s.Client, clientSet kubernetes.Clientset, - logger logr.Logger, jenkins *v1alpha2.Jenkins, config rest.Config) *BackupAndRestore { - return &BackupAndRestore{k8sClient: k8sClient, clientSet: clientSet, logger: logger, jenkins: jenkins, config: config} +func New(configuration configuration.Configuration, logger logr.Logger) *BackupAndRestore { + return &BackupAndRestore{ + Configuration: configuration, + logger: logger, + } } // Validate validates backup and restore configuration func (bar *BackupAndRestore) Validate() []string { var messages []string allContainers := map[string]v1alpha2.Container{} - for _, container := range bar.jenkins.Spec.Master.Containers { + for _, container := range bar.Configuration.Jenkins.Spec.Master.Containers { allContainers[container.Name] = container } - restore := bar.jenkins.Spec.Restore + restore := bar.Configuration.Jenkins.Spec.Restore if len(restore.ContainerName) > 0 { _, found := allContainers[restore.ContainerName] if !found { @@ -94,7 +86,7 @@ func (bar *BackupAndRestore) Validate() []string { } } - backup := bar.jenkins.Spec.Backup + backup := bar.Configuration.Jenkins.Spec.Backup if len(backup.ContainerName) > 0 { _, found := allContainers[backup.ContainerName] if !found { @@ -120,7 +112,7 @@ func (bar *BackupAndRestore) Validate() []string { // Restore performs Jenkins restore backup operation func (bar *BackupAndRestore) Restore(jenkinsClient jenkinsclient.Jenkins) error { - jenkins := bar.jenkins + jenkins := bar.Configuration.Jenkins if len(jenkins.Spec.Restore.ContainerName) == 0 || jenkins.Spec.Restore.Action.Exec == nil { bar.logger.V(log.VDebug).Info("Skipping restore backup, backup restore not configured") return nil @@ -133,7 +125,7 @@ func (bar *BackupAndRestore) Restore(jenkinsClient jenkinsclient.Jenkins) error bar.logger.V(log.VDebug).Info("Skipping restore backup") if jenkins.Status.PendingBackup == 0 { jenkins.Status.PendingBackup = 1 - return bar.k8sClient.Update(context.TODO(), jenkins) + return bar.Client.Update(context.TODO(), jenkins) } return nil } @@ -148,7 +140,7 @@ func (bar *BackupAndRestore) Restore(jenkinsClient jenkinsclient.Jenkins) error podName := resources.GetJenkinsMasterPodName(*jenkins) command := jenkins.Spec.Restore.Action.Exec.Command command = append(command, fmt.Sprintf("%d", backupNumber)) - _, _, err := bar.exec(podName, jenkins.Spec.Restore.ContainerName, command) + _, _, err := bar.Exec(podName, jenkins.Spec.Restore.ContainerName, command) if err == nil { _, err := jenkinsClient.ExecuteScript("Jenkins.instance.reload()") @@ -159,7 +151,7 @@ func (bar *BackupAndRestore) Restore(jenkinsClient jenkinsclient.Jenkins) error jenkins.Spec.Restore.RecoveryOnce = 0 jenkins.Status.RestoredBackup = backupNumber jenkins.Status.PendingBackup = backupNumber + 1 - return bar.k8sClient.Update(context.TODO(), jenkins) + return bar.Client.Update(context.TODO(), jenkins) } return err @@ -167,7 +159,7 @@ func (bar *BackupAndRestore) Restore(jenkinsClient jenkinsclient.Jenkins) error // Backup performs Jenkins backup operation func (bar *BackupAndRestore) Backup() error { - jenkins := bar.jenkins + jenkins := bar.Configuration.Jenkins if len(jenkins.Spec.Backup.ContainerName) == 0 || jenkins.Spec.Backup.Action.Exec == nil { bar.logger.V(log.VDebug).Info("Skipping restore backup, backup restore not configured") return nil @@ -181,7 +173,7 @@ func (bar *BackupAndRestore) Backup() error { podName := resources.GetJenkinsMasterPodName(*jenkins) command := jenkins.Spec.Backup.Action.Exec.Command command = append(command, fmt.Sprintf("%d", backupNumber)) - _, _, err := bar.exec(podName, jenkins.Spec.Backup.ContainerName, command) + _, _, err := bar.Exec(podName, jenkins.Spec.Backup.ContainerName, command) if err == nil { if jenkins.Status.RestoredBackup == 0 { @@ -189,7 +181,7 @@ func (bar *BackupAndRestore) Backup() error { } jenkins.Status.LastBackup = backupNumber jenkins.Status.PendingBackup = backupNumber - return bar.k8sClient.Update(context.TODO(), jenkins) + return bar.Client.Update(context.TODO(), jenkins) } return err @@ -217,9 +209,9 @@ func triggerBackup(ticker *time.Ticker, k8sClient k8s.Client, logger logr.Logger // EnsureBackupTrigger creates or update trigger which update CR to make backup func (bar *BackupAndRestore) EnsureBackupTrigger() error { - trigger, found := triggers.get(bar.jenkins.Namespace, bar.jenkins.Name) + trigger, found := triggers.get(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name) - isBackupConfigured := len(bar.jenkins.Spec.Backup.ContainerName) > 0 && bar.jenkins.Spec.Backup.Interval > 0 + isBackupConfigured := len(bar.Configuration.Jenkins.Spec.Backup.ContainerName) > 0 && bar.Configuration.Jenkins.Spec.Backup.Interval > 0 if found && !isBackupConfigured { bar.StopBackupTrigger() return nil @@ -231,7 +223,7 @@ func (bar *BackupAndRestore) EnsureBackupTrigger() error { return nil } - if found && isBackupConfigured && bar.jenkins.Spec.Backup.Interval != trigger.interval { + if found && isBackupConfigured && bar.Configuration.Jenkins.Spec.Backup.Interval != trigger.interval { bar.StopBackupTrigger() bar.startBackupTrigger() } @@ -241,55 +233,21 @@ func (bar *BackupAndRestore) EnsureBackupTrigger() error { // StopBackupTrigger stops trigger which update CR to make backup func (bar *BackupAndRestore) StopBackupTrigger() { - triggers.stop(bar.logger, bar.jenkins.Namespace, bar.jenkins.Name) + triggers.stop(bar.logger, bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name) } //IsBackupTriggerEnabled returns true if the backup trigger is enabled func (bar *BackupAndRestore) IsBackupTriggerEnabled() bool { - _, enabled := triggers.get(bar.jenkins.Namespace, bar.jenkins.Name) + _, enabled := triggers.get(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name) return enabled } func (bar *BackupAndRestore) startBackupTrigger() { bar.logger.Info("Starting backup trigger") - ticker := time.NewTicker(time.Duration(bar.jenkins.Spec.Backup.Interval) * time.Second) - triggers.add(bar.jenkins.Namespace, bar.jenkins.Name, backupTrigger{ - interval: bar.jenkins.Spec.Backup.Interval, + ticker := time.NewTicker(time.Duration(bar.Configuration.Jenkins.Spec.Backup.Interval) * time.Second) + triggers.add(bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name, backupTrigger{ + interval: bar.Configuration.Jenkins.Spec.Backup.Interval, ticker: ticker, }) - go triggerBackup(ticker, bar.k8sClient, bar.logger, bar.jenkins.Namespace, bar.jenkins.Name) -} - -func (bar *BackupAndRestore) exec(podName, containerName string, command []string) (stdout, stderr bytes.Buffer, err error) { - req := bar.clientSet.CoreV1().RESTClient().Post(). - Resource("pods"). - Name(podName). - Namespace(bar.jenkins.Namespace). - SubResource("exec") - req.VersionedParams(&corev1.PodExecOptions{ - Command: command, - Container: containerName, - Stdin: false, - Stdout: true, - Stderr: true, - TTY: false, - }, scheme.ParameterCodec) - - exec, err := remotecommand.NewSPDYExecutor(&bar.config, "POST", req.URL()) - if err != nil { - return stdout, stderr, errors.Wrap(err, "pod exec error while creating Executor") - } - - err = exec.Stream(remotecommand.StreamOptions{ - Stdin: nil, - Stdout: &stdout, - Stderr: &stderr, - Tty: false, - }) - bar.logger.V(log.VDebug).Info(fmt.Sprintf("pod exec: stdout '%s' stderr '%s'", stdout.String(), stderr.String())) - if err != nil { - return stdout, stderr, errors.Wrapf(err, "pod exec error operation on stream: stdout '%s' stderr '%s'", stdout.String(), stderr.String()) - } - - return + go triggerBackup(ticker, bar.Client, bar.logger, bar.Configuration.Jenkins.Namespace, bar.Configuration.Jenkins.Name) } diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index abd8ba9f..dd33b341 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -29,7 +29,6 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -43,16 +42,14 @@ type ReconcileJenkinsBaseConfiguration struct { configuration.Configuration logger logr.Logger jenkinsAPIConnectionSettings jenkinsclient.JenkinsAPIConnectionSettings - config *rest.Config } // New create structure which takes care of base configuration -func New(config configuration.Configuration, logger logr.Logger, jenkinsAPIConnectionSettings jenkinsclient.JenkinsAPIConnectionSettings, restConfig *rest.Config) *ReconcileJenkinsBaseConfiguration { +func New(config configuration.Configuration, logger logr.Logger, jenkinsAPIConnectionSettings jenkinsclient.JenkinsAPIConnectionSettings) *ReconcileJenkinsBaseConfiguration { return &ReconcileJenkinsBaseConfiguration{ Configuration: config, logger: logger, jenkinsAPIConnectionSettings: jenkinsAPIConnectionSettings, - config: restConfig, } } @@ -528,7 +525,7 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsMasterPod(meta metav1.O } if r.IsJenkinsTerminating(*currentJenkinsMasterPod) && r.Configuration.Jenkins.Status.UserConfigurationCompletedTime != nil { - backupAndRestore := backuprestore.New(r.Client, r.ClientSet, r.logger, r.Configuration.Jenkins, *r.config) + backupAndRestore := backuprestore.New(r.Configuration, r.logger) if backupAndRestore.IsBackupTriggerEnabled() { backupAndRestore.StopBackupTrigger() } @@ -904,6 +901,17 @@ func (r *ReconcileJenkinsBaseConfiguration) waitForJenkins() (reconcile.Result, } func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient() (jenkinsclient.Jenkins, error) { + switch r.Configuration.Jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy { + case v1alpha2.ServiceAccountAuthorizationStrategy: + return r.ensureJenkinsClientFromServiceAccount() + case v1alpha2.CreateUserAuthorizationStrategy: + return r.ensureJenkinsClientFromSecret() + default: + return nil, stackerr.Errorf("unrecognized '%s' spec.jenkinsAPISettings.authorizationStrategy", r.Configuration.Jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy) + } +} + +func (r *ReconcileJenkinsBaseConfiguration) getJenkinsAPIUrl() (string, error) { var service corev1.Service err := r.Client.Get(context.TODO(), types.NamespacedName{ @@ -912,7 +920,7 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient() (jenkinsclient }, &service) if err != nil { - return nil, err + return "", err } jenkinsURL := r.jenkinsAPIConnectionSettings.BuildJenkinsAPIUrl(service.Name, service.Namespace, service.Spec.Ports[0].Port, service.Spec.Ports[0].NodePort) @@ -921,6 +929,30 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient() (jenkinsclient jenkinsURL = jenkinsURL + prefix } + return jenkinsURL, nil +} + +func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClientFromServiceAccount() (jenkinsclient.Jenkins, error) { + jenkinsAPIUrl, err := r.getJenkinsAPIUrl() + if err != nil { + return nil, err + } + + podName := resources.GetJenkinsMasterPodName(*r.Configuration.Jenkins) + token, _, err := r.Configuration.Exec(podName, resources.JenkinsMasterContainerName, []string{"cat", "/var/run/secrets/kubernetes.io/serviceaccount/token"}) + if err != nil { + return nil, err + } + + return jenkinsclient.NewBearerTokenAuthorization(jenkinsAPIUrl, token.String()) +} + +func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClientFromSecret() (jenkinsclient.Jenkins, error) { + jenkinsURL, err := r.getJenkinsAPIUrl() + if err != nil { + return nil, err + } + r.logger.V(log.VDebug).Info(fmt.Sprintf("Jenkins API URL '%s'", jenkinsURL)) credentialsSecret := &corev1.Secret{} @@ -948,7 +980,7 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient() (jenkinsclient currentJenkinsMasterPod.ObjectMeta.CreationTimestamp.Time.UTC().After(tokenCreationTime.UTC()) { r.logger.Info("Generating Jenkins API token for operator") userName := string(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey]) - jenkinsClient, err := jenkinsclient.New( + jenkinsClient, err := jenkinsclient.NewUserAndPasswordAuthorization( jenkinsURL, userName, string(credentialsSecret.Data[resources.OperatorCredentialsSecretPasswordKey])) @@ -970,7 +1002,7 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureJenkinsClient() (jenkinsclient } } - return jenkinsclient.New( + return jenkinsclient.NewUserAndPasswordAuthorization( jenkinsURL, string(credentialsSecret.Data[resources.OperatorCredentialsSecretUserNameKey]), string(credentialsSecret.Data[resources.OperatorCredentialsSecretTokenKey])) diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 2b60d09f..8a4bdb9e 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -240,7 +240,7 @@ func TestCompareVolumes(t *testing.T) { Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins), }, } - reconciler := New(configuration.Configuration{Jenkins: jenkins}, nil, client.JenkinsAPIConnectionSettings{}, nil) + reconciler := New(configuration.Configuration{Jenkins: jenkins}, nil, client.JenkinsAPIConnectionSettings{}) got := reconciler.compareVolumes(pod) @@ -264,7 +264,7 @@ func TestCompareVolumes(t *testing.T) { Volumes: resources.GetJenkinsMasterPodBaseVolumes(jenkins), }, } - reconciler := New(configuration.Configuration{Jenkins: jenkins}, nil, client.JenkinsAPIConnectionSettings{}, nil) + reconciler := New(configuration.Configuration{Jenkins: jenkins}, nil, client.JenkinsAPIConnectionSettings{}) got := reconciler.compareVolumes(pod) @@ -288,7 +288,7 @@ func TestCompareVolumes(t *testing.T) { Volumes: append(resources.GetJenkinsMasterPodBaseVolumes(jenkins), corev1.Volume{Name: "added"}), }, } - reconciler := New(configuration.Configuration{Jenkins: jenkins}, nil, client.JenkinsAPIConnectionSettings{}, nil) + reconciler := New(configuration.Configuration{Jenkins: jenkins}, nil, client.JenkinsAPIConnectionSettings{}) got := reconciler.compareVolumes(pod) @@ -766,7 +766,7 @@ func TestEnsureExtraRBAC(t *testing.T) { Roles: []rbacv1.RoleRef{}, }, } - reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}, nil) + reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}) metaObject := resources.NewResourceObjectMeta(jenkins) // when @@ -802,7 +802,7 @@ func TestEnsureExtraRBAC(t *testing.T) { }, }, } - reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}, nil) + reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}) metaObject := resources.NewResourceObjectMeta(jenkins) // when @@ -844,7 +844,7 @@ func TestEnsureExtraRBAC(t *testing.T) { }, }, } - reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}, nil) + reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}) metaObject := resources.NewResourceObjectMeta(jenkins) // when @@ -887,7 +887,7 @@ func TestEnsureExtraRBAC(t *testing.T) { }, }, } - reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, log.Log, client.JenkinsAPIConnectionSettings{}, nil) + reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, log.Log, client.JenkinsAPIConnectionSettings{}) metaObject := resources.NewResourceObjectMeta(jenkins) // when diff --git a/pkg/controller/jenkins/configuration/base/resources/init_configuration_configmap.go b/pkg/controller/jenkins/configuration/base/resources/init_configuration_configmap.go index f21d5b16..bddad1cb 100644 --- a/pkg/controller/jenkins/configuration/base/resources/init_configuration_configmap.go +++ b/pkg/controller/jenkins/configuration/base/resources/init_configuration_configmap.go @@ -17,6 +17,7 @@ const createOperatorUserFileName = "createOperatorUser.groovy" var createOperatorUserGroovyFmtTemplate = template.Must(template.New(createOperatorUserFileName).Parse(` import hudson.security.* +{{- if .Enable }} def jenkins = jenkins.model.Jenkins.getInstance() def operatorUserCreatedFile = new File('{{ .OperatorUserCreatedFilePath }}') @@ -34,15 +35,18 @@ if (!operatorUserCreatedFile.exists()) { operatorUserCreatedFile.createNewFile() } +{{- end }} `)) func buildCreateJenkinsOperatorUserGroovyScript(jenkins *v1alpha2.Jenkins) (*string, error) { data := struct { + Enable bool OperatorCredentialsPath string OperatorUserNameFile string OperatorPasswordFile string OperatorUserCreatedFilePath string }{ + Enable: jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy == v1alpha2.CreateUserAuthorizationStrategy, OperatorCredentialsPath: jenkinsOperatorCredentialsVolumePath, OperatorUserNameFile: OperatorCredentialsSecretUserNameKey, OperatorPasswordFile: OperatorCredentialsSecretPasswordKey, diff --git a/pkg/controller/jenkins/configuration/base/validate.go b/pkg/controller/jenkins/configuration/base/validate.go index 3155d379..cd12eb71 100644 --- a/pkg/controller/jenkins/configuration/base/validate.go +++ b/pkg/controller/jenkins/configuration/base/validate.go @@ -62,6 +62,10 @@ func (r *ReconcileJenkinsBaseConfiguration) Validate(jenkins *v1alpha2.Jenkins) messages = append(messages, msg...) } + if jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy != v1alpha2.CreateUserAuthorizationStrategy && jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy != v1alpha2.ServiceAccountAuthorizationStrategy { + messages = append(messages, fmt.Sprintf("unrecognized '%s' spec.jenkinsAPISettings.authorizationStrategy", jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy)) + } + return messages, nil } diff --git a/pkg/controller/jenkins/configuration/base/validate_test.go b/pkg/controller/jenkins/configuration/base/validate_test.go index a5f131b3..39c455d0 100644 --- a/pkg/controller/jenkins/configuration/base/validate_test.go +++ b/pkg/controller/jenkins/configuration/base/validate_test.go @@ -23,7 +23,7 @@ import ( func TestValidatePlugins(t *testing.T) { log.SetupLogger(true) - baseReconcileLoop := New(configuration.Configuration{}, log.Log, client.JenkinsAPIConnectionSettings{}, nil) + baseReconcileLoop := New(configuration.Configuration{}, log.Log, client.JenkinsAPIConnectionSettings{}) t.Run("empty", func(t *testing.T) { var requiredBasePlugins []plugins.Plugin var basePlugins []v1alpha2.Plugin @@ -166,7 +166,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, err := baseReconcileLoop.validateImagePullSecrets() fmt.Println(got) @@ -190,7 +190,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: &jenkins, - }, nil, client.JenkinsAPIConnectionSettings{}, nil) + }, nil, client.JenkinsAPIConnectionSettings{}) got, _ := baseReconcileLoop.validateImagePullSecrets() @@ -226,7 +226,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: &jenkins, - }, nil, client.JenkinsAPIConnectionSettings{}, nil) + }, nil, client.JenkinsAPIConnectionSettings{}) got, _ := baseReconcileLoop.validateImagePullSecrets() @@ -262,7 +262,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, _ := baseReconcileLoop.validateImagePullSecrets() @@ -298,7 +298,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, _ := baseReconcileLoop.validateImagePullSecrets() @@ -334,7 +334,7 @@ func TestReconcileJenkinsBaseConfiguration_validateImagePullSecrets(t *testing.T baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, _ := baseReconcileLoop.validateImagePullSecrets() @@ -367,7 +367,7 @@ func TestValidateJenkinsMasterPodEnvs(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateJenkinsMasterPodEnvs() assert.Nil(t, got) }) @@ -390,7 +390,7 @@ func TestValidateJenkinsMasterPodEnvs(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateJenkinsMasterPodEnvs() assert.Equal(t, got, []string{"Jenkins Master container env 'JAVA_OPTS' doesn't have required flag '-Djava.awt.headless=true'"}) @@ -414,7 +414,7 @@ func TestValidateJenkinsMasterPodEnvs(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateJenkinsMasterPodEnvs() assert.Equal(t, got, []string{"Jenkins Master container env 'JAVA_OPTS' doesn't have required flag '-Djenkins.install.runSetupWizard=false'"}) @@ -436,7 +436,7 @@ func TestValidateReservedVolumes(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateReservedVolumes() assert.Nil(t, got) }) @@ -454,7 +454,7 @@ func TestValidateReservedVolumes(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateReservedVolumes() assert.Equal(t, got, []string{"Jenkins Master pod volume 'jenkins-home' is reserved please choose different one"}) @@ -470,7 +470,7 @@ func TestValidateContainerVolumeMounts(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateContainerVolumeMounts(v1alpha2.Container{}) assert.Nil(t, got) }) @@ -498,7 +498,7 @@ func TestValidateContainerVolumeMounts(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0]) assert.Nil(t, got) }) @@ -526,7 +526,7 @@ func TestValidateContainerVolumeMounts(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0]) assert.Equal(t, got, []string{"mountPath not set for 'example' volume mount in container ''"}) }) @@ -549,7 +549,7 @@ func TestValidateContainerVolumeMounts(t *testing.T) { } baseReconcileLoop := New(configuration.Configuration{ Jenkins: &jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got := baseReconcileLoop.validateContainerVolumeMounts(jenkins.Spec.Master.Containers[0]) assert.Equal(t, got, []string{"Not found volume for 'missing-volume' volume mount in container ''"}) @@ -571,7 +571,7 @@ func TestValidateConfigMapVolume(t *testing.T) { fakeClient := fake.NewFakeClient() baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, err := baseReconcileLoop.validateConfigMapVolume(volume) @@ -599,7 +599,7 @@ func TestValidateConfigMapVolume(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, err := baseReconcileLoop.validateConfigMapVolume(volume) @@ -625,7 +625,7 @@ func TestValidateConfigMapVolume(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, err := baseReconcileLoop.validateConfigMapVolume(volume) @@ -650,7 +650,7 @@ func TestValidateSecretVolume(t *testing.T) { fakeClient := fake.NewFakeClient() baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, err := baseReconcileLoop.validateSecretVolume(volume) @@ -676,7 +676,7 @@ func TestValidateSecretVolume(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, err := baseReconcileLoop.validateSecretVolume(volume) @@ -700,7 +700,7 @@ func TestValidateSecretVolume(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Client: fakeClient, Jenkins: jenkins, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, err := baseReconcileLoop.validateSecretVolume(volume) assert.NoError(t, err) @@ -724,7 +724,7 @@ func TestValidateCustomization(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Jenkins: jenkins, Client: fakeClient, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) got, err := baseReconcileLoop.validateCustomization(customization, "spec.groovyScripts") @@ -746,7 +746,7 @@ func TestValidateCustomization(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Jenkins: jenkins, Client: fakeClient, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) err := fakeClient.Create(context.TODO(), secret) require.NoError(t, err) @@ -777,7 +777,7 @@ func TestValidateCustomization(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Jenkins: jenkins, Client: fakeClient, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) err := fakeClient.Create(context.TODO(), secret) require.NoError(t, err) err = fakeClient.Create(context.TODO(), configMap) @@ -804,7 +804,7 @@ func TestValidateCustomization(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Jenkins: jenkins, Client: fakeClient, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) err := fakeClient.Create(context.TODO(), configMap) require.NoError(t, err) @@ -829,7 +829,7 @@ func TestValidateCustomization(t *testing.T) { baseReconcileLoop := New(configuration.Configuration{ Jenkins: jenkins, Client: fakeClient, - }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}, nil) + }, logf.ZapLogger(false), client.JenkinsAPIConnectionSettings{}) err := fakeClient.Create(context.TODO(), secret) require.NoError(t, err) diff --git a/pkg/controller/jenkins/configuration/configuration.go b/pkg/controller/jenkins/configuration/configuration.go index 4bb21f07..4c6e7bd2 100644 --- a/pkg/controller/jenkins/configuration/configuration.go +++ b/pkg/controller/jenkins/configuration/configuration.go @@ -1,6 +1,7 @@ package configuration import ( + "bytes" "context" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" @@ -15,6 +16,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) @@ -26,6 +30,7 @@ type Configuration struct { Notifications *chan event.Event Jenkins *v1alpha2.Jenkins Scheme *runtime.Scheme + Config *rest.Config } // RestartJenkinsMasterPod terminate Jenkins master pod and notifies about it @@ -111,3 +116,37 @@ func (c *Configuration) CreateOrUpdateResource(obj metav1.Object) error { return nil } + +// Exec executes command in the given pod and it's container +func (c *Configuration) Exec(podName, containerName string, command []string) (stdout, stderr bytes.Buffer, err error) { + req := c.ClientSet.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(podName). + Namespace(c.Jenkins.Namespace). + SubResource("exec") + req.VersionedParams(&corev1.PodExecOptions{ + Command: command, + Container: containerName, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: false, + }, scheme.ParameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(c.Config, "POST", req.URL()) + if err != nil { + return stdout, stderr, stackerr.Wrap(err, "pod exec error while creating Executor") + } + + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: nil, + Stdout: &stdout, + Stderr: &stderr, + Tty: false, + }) + if err != nil { + return stdout, stderr, stackerr.Wrapf(err, "pod exec error operation on stream: stdout '%s' stderr '%s'", stdout.String(), stderr.String()) + } + + return +} diff --git a/pkg/controller/jenkins/configuration/user/reconcile.go b/pkg/controller/jenkins/configuration/user/reconcile.go index b6af504d..8a719b30 100644 --- a/pkg/controller/jenkins/configuration/user/reconcile.go +++ b/pkg/controller/jenkins/configuration/user/reconcile.go @@ -12,7 +12,6 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/groovy" "github.com/go-logr/logr" - "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -21,22 +20,20 @@ type ReconcileUserConfiguration struct { configuration.Configuration jenkinsClient jenkinsclient.Jenkins logger logr.Logger - config rest.Config } // New create structure which takes care of user configuration -func New(configuration configuration.Configuration, jenkinsClient jenkinsclient.Jenkins, logger logr.Logger, config rest.Config) *ReconcileUserConfiguration { +func New(configuration configuration.Configuration, jenkinsClient jenkinsclient.Jenkins, logger logr.Logger) *ReconcileUserConfiguration { return &ReconcileUserConfiguration{ Configuration: configuration, jenkinsClient: jenkinsClient, logger: logger, - config: config, } } // Reconcile it's a main reconciliation loop for user supplied configuration func (r *ReconcileUserConfiguration) Reconcile() (reconcile.Result, error) { - backupAndRestore := backuprestore.New(r.Client, r.ClientSet, r.logger, r.Configuration.Jenkins, r.config) + backupAndRestore := backuprestore.New(r.Configuration, r.logger) result, err := r.ensureSeedJobs() if err != nil { diff --git a/pkg/controller/jenkins/configuration/user/validate.go b/pkg/controller/jenkins/configuration/user/validate.go index 520603bb..1a163d70 100644 --- a/pkg/controller/jenkins/configuration/user/validate.go +++ b/pkg/controller/jenkins/configuration/user/validate.go @@ -8,7 +8,7 @@ import ( // Validate validates Jenkins CR Spec section func (r *ReconcileUserConfiguration) Validate(jenkins *v1alpha2.Jenkins) ([]string, error) { - backupAndRestore := backuprestore.New(r.Client, r.ClientSet, r.logger, r.Configuration.Jenkins, r.config) + backupAndRestore := backuprestore.New(r.Configuration, r.logger) if msg := backupAndRestore.Validate(); msg != nil { return msg, nil } diff --git a/pkg/controller/jenkins/jenkins_controller.go b/pkg/controller/jenkins/jenkins_controller.go index c9e5b884..e66ad130 100644 --- a/pkg/controller/jenkins/jenkins_controller.go +++ b/pkg/controller/jenkins/jenkins_controller.go @@ -232,10 +232,11 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg Notifications: r.notificationEvents, Jenkins: jenkins, Scheme: r.scheme, + Config: &r.config, } // Reconcile base configuration - baseConfiguration := base.New(config, logger, r.jenkinsAPIConnectionSettings, &r.config) + baseConfiguration := base.New(config, logger, r.jenkinsAPIConnectionSettings) var baseMessages []string baseMessages, err = baseConfiguration.Validate(jenkins) @@ -289,7 +290,7 @@ func (r *ReconcileJenkins) reconcile(request reconcile.Request, logger logr.Logg logger.Info(message) } // Reconcile user configuration - userConfiguration := user.New(config, jenkinsClient, logger, r.config) + userConfiguration := user.New(config, jenkinsClient, logger) var messages []string messages, err = userConfiguration.Validate(jenkins) @@ -496,6 +497,18 @@ func (r *ReconcileJenkins) setDefaults(jenkins *v1alpha2.Jenkins, logger logr.Lo jenkins.Spec.Master.SecurityContext = &securityContext } + if reflect.DeepEqual(jenkins.Spec.JenkinsAPISettings, v1alpha2.JenkinsAPISettings{}) { + logger.Info("Setting default Jenkins API settings") + changed = true + jenkins.Spec.JenkinsAPISettings = v1alpha2.JenkinsAPISettings{AuthorizationStrategy: v1alpha2.CreateUserAuthorizationStrategy} + } + + if jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy == "" { + logger.Info("Setting default Jenkins API settings authorization strategy") + changed = true + jenkins.Spec.JenkinsAPISettings.AuthorizationStrategy = v1alpha2.CreateUserAuthorizationStrategy + } + if changed { return changed, errors.WithStack(r.client.Update(context.TODO(), jenkins)) } diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index cc197e56..c09d5ab1 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -70,7 +70,7 @@ func createJenkinsAPIClient(jenkins *v1alpha2.Jenkins, hostname string, port int UseNodePort: useNodePort, }.BuildJenkinsAPIUrl(service.Name, service.Namespace, service.Spec.Ports[0].Port, service.Spec.Ports[0].NodePort) - return jenkinsclient.New( + return jenkinsclient.NewUserAndPasswordAuthorization( jenkinsAPIURL, string(adminSecret.Data[resources.OperatorCredentialsSecretUserNameKey]), string(adminSecret.Data[resources.OperatorCredentialsSecretTokenKey]),