package sa import ( "errors" "fmt" "io" "math" "os" "time" "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/fields" "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/watch" "github.com/openshift/origin/pkg/cmd/templates" "github.com/openshift/origin/pkg/cmd/util/clientcmd" "github.com/openshift/origin/pkg/cmd/util/term" "github.com/openshift/origin/pkg/serviceaccounts" osautil "github.com/openshift/origin/pkg/serviceaccounts/util" ) const ( NewServiceAccountTokenRecommendedName = "new-token" newServiceAccountTokenShort = `Generate a new token for a service account.` newServiceAccountTokenUsage = `%s SA-NAME` ) var ( newServiceAccountTokenLong = templates.LongDesc(` Generate a new token for a service account. Service account API tokens are used by service accounts to authenticate to the API. This command will generate a new token, which could be used to compartmentalize service account actions by executing them with distinct tokens, to rotate out pre-existing token on the service account, or for use by an external client. If a label is provided, it will be applied to any created token so that tokens created with this command can be idenitifed.`) newServiceAccountTokenExamples = templates.Examples(` # Generate a new token for service account 'default' %[1]s 'default' # Generate a new token for service account 'default' and apply # labels 'foo' and 'bar' to the new token for identification # %[1]s 'default' --labels foo=foo-value,bar=bar-value`) ) type NewServiceAccountTokenOptions struct { SAName string SAClient unversioned.ServiceAccountsInterface SecretsClient unversioned.SecretsInterface Labels map[string]string Timeout time.Duration Out io.Writer Err io.Writer } func NewCommandNewServiceAccountToken(name, fullname string, f *clientcmd.Factory, out io.Writer) *cobra.Command { options := &NewServiceAccountTokenOptions{ Out: out, Err: os.Stderr, Labels: map[string]string{}, } var requestedLabels string newServiceAccountTokenCommand := &cobra.Command{ Use: fmt.Sprintf(newServiceAccountTokenUsage, name), Short: newServiceAccountTokenShort, Long: newServiceAccountTokenLong, Example: fmt.Sprintf(newServiceAccountTokenExamples, fullname), Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(options.Complete(args, requestedLabels, f, cmd)) cmdutil.CheckErr(options.Validate()) cmdutil.CheckErr(options.Run()) }, } newServiceAccountTokenCommand.Flags().DurationVar(&options.Timeout, "timeout", 30*time.Second, "the maximum time allowed to generate a token") newServiceAccountTokenCommand.Flags().StringVarP(&requestedLabels, "labels", "l", "", "labels to set in all resources for this application, given as a comma-delimited list of key-value pairs") return newServiceAccountTokenCommand } func (o *NewServiceAccountTokenOptions) Complete(args []string, requestedLabels string, f *clientcmd.Factory, cmd *cobra.Command) error { if len(args) != 1 { return cmdutil.UsageError(cmd, fmt.Sprintf("expected one service account name as an argument, got %q", args)) } o.SAName = args[0] if len(requestedLabels) > 0 { labels, err := kubectl.ParseLabels(requestedLabels) if err != nil { return cmdutil.UsageError(cmd, err.Error()) } o.Labels = labels } client, err := f.Client() if err != nil { return err } namespace, _, err := f.DefaultNamespace() if err != nil { return fmt.Errorf("could not retrieve default namespace: %v", err) } o.SAClient = client.ServiceAccounts(namespace) o.SecretsClient = client.Secrets(namespace) return nil } func (o *NewServiceAccountTokenOptions) Validate() error { if o.SAName == "" { return errors.New("service account name cannot be empty") } if o.SAClient == nil || o.SecretsClient == nil { return errors.New("API clients must not be nil in order to create a new service account token") } if o.Timeout <= 0 { return errors.New("a positive amount of time must be allotted for the timeout") } if o.Out == nil || o.Err == nil { return errors.New("cannot proceed if output or error writers are nil") } return nil } // Run creates a new token secret, waits for the service account token controller to fulfill it, then adds the token to the service account func (o *NewServiceAccountTokenOptions) Run() error { serviceAccount, err := o.SAClient.Get(o.SAName) if err != nil { return err } tokenSecret := &api.Secret{ ObjectMeta: api.ObjectMeta{ GenerateName: osautil.GetTokenSecretNamePrefix(serviceAccount), Namespace: serviceAccount.Namespace, Labels: o.Labels, Annotations: map[string]string{ api.ServiceAccountNameKey: serviceAccount.Name, }, }, Type: api.SecretTypeServiceAccountToken, Data: map[string][]byte{}, } persistedToken, err := o.SecretsClient.Create(tokenSecret) if err != nil { return err } // we need to wait for the service account token controller to make the new token valid tokenSecret, err = waitForToken(persistedToken, serviceAccount, o.Timeout, o.SecretsClient) if err != nil { return err } token, exists := tokenSecret.Data[api.ServiceAccountTokenKey] if !exists { return fmt.Errorf("service account token %q did not contain token data", tokenSecret.Name) } fmt.Fprintf(o.Out, string(token)) if term.IsTerminalWriter(o.Out) { // pretty-print for a TTY fmt.Fprintf(o.Out, "\n") } return nil } // waitForToken uses `cmd.Until` to wait for the service account controller to fulfill the token request func waitForToken(token *api.Secret, serviceAccount *api.ServiceAccount, timeout time.Duration, client unversioned.SecretsInterface) (*api.Secret, error) { // there is no provided rounding function, so we use Round(x) === Floor(x + 0.5) timeoutSeconds := int64(math.Floor(timeout.Seconds() + 0.5)) options := api.ListOptions{ FieldSelector: fields.SelectorFromSet(fields.Set(map[string]string{"metadata.name": token.Name})), Watch: true, ResourceVersion: token.ResourceVersion, TimeoutSeconds: &timeoutSeconds, } watcher, err := client.Watch(options) if err != nil { return nil, fmt.Errorf("could not begin watch for token: %v", err) } event, err := watch.Until(timeout, watcher, func(event watch.Event) (bool, error) { if event.Type == watch.Error { return false, fmt.Errorf("encountered error while watching for token: %v", event.Object) } eventToken, ok := event.Object.(*api.Secret) if !ok { return false, nil } if eventToken.Name != token.Name { return false, nil } switch event.Type { case watch.Modified: if serviceaccounts.IsValidServiceAccountToken(serviceAccount, eventToken) { return true, nil } case watch.Deleted: return false, errors.New("token was deleted before fulfillment by service account token controller") case watch.Added: return false, errors.New("unxepected action: token was added after initial creation") } return false, nil }) if err != nil { return nil, err } return event.Object.(*api.Secret), nil }