package policy import ( "errors" "fmt" "io" "sort" "github.com/spf13/cobra" kapi "k8s.io/kubernetes/pkg/api" kapierrors "k8s.io/kubernetes/pkg/api/errors" kcoreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/unversioned" kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" sccutil "k8s.io/kubernetes/pkg/securitycontextconstraints/util" "k8s.io/kubernetes/pkg/util/sets" "github.com/openshift/origin/pkg/cmd/server/bootstrappolicy" "github.com/openshift/origin/pkg/cmd/templates" cmdutil "github.com/openshift/origin/pkg/cmd/util" "github.com/openshift/origin/pkg/cmd/util/clientcmd" ) // ReconcileSCCRecommendedName is the recommended command name const ReconcileSCCRecommendedName = "reconcile-sccs" type ReconcileSCCOptions struct { // confirmed indicates that the data should be persisted Confirmed bool // union controls if we make additive changes to the users/groups fields or overwrite them // as well as preserving existing priorities (unset priorities will always be reconciled) Union bool // is the name of the openshift infrastructure namespace. It is provided here so that // the command doesn't need to try and parse the policy config. InfraNamespace string Out io.Writer Output string SCCClient kcoreclient.SecurityContextConstraintsInterface NSClient kcoreclient.NamespaceInterface } var ( reconcileSCCLong = templates.LongDesc(` Replace cluster SCCs to match the recommended bootstrap policy This command will inspect the cluster SCCs against the recommended bootstrap SCCs. Any cluster SCC that does not match will be replaced by the recommended SCC. This command will not remove any additional cluster SCCs. By default, this command will not remove additional users and groups that have been granted access to the SCC and will preserve existing priorities (but will always reconcile unset priorities and the policy definition). You can see which cluster SCCs have recommended changes by choosing an output type.`) reconcileSCCExample = templates.Examples(` # Display the cluster SCCs that would be modified %[1]s # Update cluster SCCs that don't match the current defaults preserving additional grants # for users and group and keeping any priorities that are already set %[1]s --confirm # Replace existing users, groups, and priorities that do not match defaults %[1]s --additive-only=false --confirm`) ) // NewDefaultReconcileSCCOptions provides a ReconcileSCCOptions with default settings. func NewDefaultReconcileSCCOptions() *ReconcileSCCOptions { return &ReconcileSCCOptions{ Union: true, InfraNamespace: bootstrappolicy.DefaultOpenShiftInfraNamespace, } } // NewCmdReconcileSCC implements the OpenShift cli reconcile-sccs command. func NewCmdReconcileSCC(name, fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command { o := NewDefaultReconcileSCCOptions() o.Out = out cmd := &cobra.Command{ Use: name, Short: "Replace cluster SCCs to match the recommended bootstrap policy", Long: reconcileSCCLong, Example: fmt.Sprintf(reconcileSCCExample, fullName), Run: func(cmd *cobra.Command, args []string) { if err := o.Complete(cmd, f, args); err != nil { kcmdutil.CheckErr(err) } if err := o.Validate(); err != nil { kcmdutil.CheckErr(kcmdutil.UsageError(cmd, err.Error())) } if err := o.RunReconcileSCCs(cmd, f); err != nil { kcmdutil.CheckErr(err) } }, } cmd.Flags().BoolVar(&o.Confirmed, "confirm", o.Confirmed, "Specify that cluster SCCs should be modified. Defaults to false, displaying what would be replaced but not actually replacing anything.") cmd.Flags().BoolVar(&o.Union, "additive-only", o.Union, "Preserves extra users, groups, labels and annotations in the SCC as well as existing priorities.") cmd.Flags().StringVar(&o.InfraNamespace, "infrastructure-namespace", o.InfraNamespace, "Name of the infrastructure namespace.") kcmdutil.AddPrinterFlags(cmd) cmd.Flags().Lookup("output").DefValue = "yaml" cmd.Flags().Lookup("output").Value.Set("yaml") return cmd } func (o *ReconcileSCCOptions) Complete(cmd *cobra.Command, f *clientcmd.Factory, args []string) error { if len(args) != 0 { return kcmdutil.UsageError(cmd, "no arguments are allowed") } _, _, kClient, err := f.Clients() if err != nil { return err } o.SCCClient = kClient.Core().SecurityContextConstraints() o.NSClient = kClient.Core().Namespaces() o.Output = kcmdutil.GetFlagString(cmd, "output") return nil } func (o *ReconcileSCCOptions) Validate() error { if o.SCCClient == nil { return errors.New("a SCC client is required") } if _, err := o.NSClient.Get(o.InfraNamespace); err != nil { return fmt.Errorf("%s is not a valid namespace", o.InfraNamespace) } return nil } // RunReconcileSCCs contains the functionality for the reconcile-sccs command for making or // previewing changes. func (o *ReconcileSCCOptions) RunReconcileSCCs(cmd *cobra.Command, f *clientcmd.Factory) error { // get sccs that need updated changedSCCs, err := o.ChangedSCCs() if err != nil { return err } if len(changedSCCs) == 0 { return nil } if !o.Confirmed { list := &kapi.List{} for _, item := range changedSCCs { list.Items = append(list.Items, item) } mapper, _ := f.Object(false) fn := cmdutil.VersionedPrintObject(f.PrintObject, cmd, mapper, o.Out) if err := fn(list); err != nil { return err } } if o.Confirmed { return o.ReplaceChangedSCCs(changedSCCs) } return nil } // ChangedSCCs returns the SCCs that must be created and/or updated to match the // recommended bootstrap SCCs. func (o *ReconcileSCCOptions) ChangedSCCs() ([]*kapi.SecurityContextConstraints, error) { changedSCCs := []*kapi.SecurityContextConstraints{} groups, users := bootstrappolicy.GetBoostrapSCCAccess(o.InfraNamespace) bootstrapSCCs := bootstrappolicy.GetBootstrapSecurityContextConstraints(groups, users) for i := range bootstrapSCCs { expectedSCC := &bootstrapSCCs[i] actualSCC, err := o.SCCClient.Get(expectedSCC.Name) // if not found it needs to be created if kapierrors.IsNotFound(err) { changedSCCs = append(changedSCCs, expectedSCC) continue } if err != nil { return nil, err } // if found then we need to diff to see if it needs updated if updatedSCC, needsUpdating := o.computeUpdatedSCC(*expectedSCC, *actualSCC); needsUpdating { changedSCCs = append(changedSCCs, updatedSCC) } } return changedSCCs, nil } // ReplaceChangedSCCs persists the changed SCCs. func (o *ReconcileSCCOptions) ReplaceChangedSCCs(changedSCCs []*kapi.SecurityContextConstraints) error { for i := range changedSCCs { _, err := o.SCCClient.Get(changedSCCs[i].Name) if err != nil && !kapierrors.IsNotFound(err) { return err } if kapierrors.IsNotFound(err) { createdSCC, err := o.SCCClient.Create(changedSCCs[i]) if err != nil { return err } fmt.Fprintf(o.Out, "securitycontextconstraints/%s\n", createdSCC.Name) continue } updatedSCC, err := o.SCCClient.Update(changedSCCs[i]) if err != nil { return err } fmt.Fprintf(o.Out, "securitycontextconstraints/%s\n", updatedSCC.Name) } return nil } // computeUpdatedSCC determines if the expected SCC looks like the actual SCC // it does this by making the expected SCC mirror the actual SCC for items that // we are not reconciling and performing a diff (ignoring changes to metadata). // If a diff is produced then the expected SCC is submitted as needing an update. func (o *ReconcileSCCOptions) computeUpdatedSCC(expected kapi.SecurityContextConstraints, actual kapi.SecurityContextConstraints) (*kapi.SecurityContextConstraints, bool) { needsUpdate := false // if unioning old and new groups/users then make the expected contain all // also preserve and set priorities if o.Union { groupSet := sets.NewString(actual.Groups...) groupSet.Insert(expected.Groups...) expected.Groups = groupSet.List() userSet := sets.NewString(actual.Users...) userSet.Insert(expected.Users...) expected.Users = userSet.List() if actual.Priority != nil { expected.Priority = actual.Priority } // preserve labels and annotations expected.Labels = MergeMaps(expected.Labels, actual.Labels) expected.Annotations = MergeMaps(expected.Annotations, actual.Annotations) } // sort volumes to remove variants in order sortVolumes(&expected) sortVolumes(&actual) // sort users and groups to remove any variants in order when diffing sort.StringSlice(actual.Groups).Sort() sort.StringSlice(actual.Users).Sort() sort.StringSlice(expected.Groups).Sort() sort.StringSlice(expected.Users).Sort() // compute the updated scc as follows: // 1. start with the expected scc // 2. take the objectmeta from the actual scc (preserves the resource version and uid) // 3. add back the labels and annotations from the expected scc (which were already merged if unioning was desired) updated := expected updated.ObjectMeta = actual.ObjectMeta updated.ObjectMeta.Labels = expected.Labels updated.ObjectMeta.Annotations = expected.Annotations if !kapi.Semantic.DeepEqual(updated, actual) { needsUpdate = true } return &updated, needsUpdate } // sortVolumes sorts the volume slice of the SCC in place. func sortVolumes(scc *kapi.SecurityContextConstraints) { if scc.Volumes == nil || len(scc.Volumes) == 0 { return } volumes := sccutil.FSTypeToStringSet(scc.Volumes).List() sort.StringSlice(volumes).Sort() scc.Volumes = sliceToFSType(volumes) } // sliceToFSType converts a string slice into FStypes. func sliceToFSType(s []string) []kapi.FSType { fsTypes := []kapi.FSType{} for _, v := range s { fsTypes = append(fsTypes, kapi.FSType(v)) } return fsTypes } // MergeMaps will merge to map[string]string instances, with // keys from the second argument overwriting keys from the // first argument, in case of duplicates. func MergeMaps(a, b map[string]string) map[string]string { if a == nil && b == nil { return nil } res := make(map[string]string) for k, v := range a { res[k] = v } for k, v := range b { res[k] = v } return res }