| ... | ... |
@@ -29,6 +29,8 @@ import ( |
| 29 | 29 |
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" |
| 30 | 30 |
kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" |
| 31 | 31 |
kmaster "github.com/GoogleCloudPlatform/kubernetes/pkg/master" |
| 32 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator" |
|
| 33 |
+ etcdallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator/etcd" |
|
| 32 | 34 |
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount" |
| 33 | 35 |
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" |
| 34 | 36 |
|
| ... | ... |
@@ -85,6 +87,10 @@ import ( |
| 85 | 85 |
routeregistry "github.com/openshift/origin/pkg/route/registry/route" |
| 86 | 86 |
clusternetworketcd "github.com/openshift/origin/pkg/sdn/registry/clusternetwork/etcd" |
| 87 | 87 |
hostsubnetetcd "github.com/openshift/origin/pkg/sdn/registry/hostsubnet/etcd" |
| 88 |
+ securitycontroller "github.com/openshift/origin/pkg/security/controller" |
|
| 89 |
+ "github.com/openshift/origin/pkg/security/mcs" |
|
| 90 |
+ "github.com/openshift/origin/pkg/security/uid" |
|
| 91 |
+ "github.com/openshift/origin/pkg/security/uidallocator" |
|
| 88 | 92 |
"github.com/openshift/origin/pkg/service" |
| 89 | 93 |
templateregistry "github.com/openshift/origin/pkg/template/registry" |
| 90 | 94 |
templateetcd "github.com/openshift/origin/pkg/template/registry/etcd" |
| ... | ... |
@@ -992,6 +998,40 @@ func (c *MasterConfig) RunImageImportController() {
|
| 992 | 992 |
controller.Run() |
| 993 | 993 |
} |
| 994 | 994 |
|
| 995 |
+func (c *MasterConfig) RunSecurityAllocationController() {
|
|
| 996 |
+ uidRange, err := uid.ParseRange("1000000000-1999999999/10000") // provide 100k uid blocks
|
|
| 997 |
+ if err != nil {
|
|
| 998 |
+ glog.Fatalf("Unable to describe UID range: %v", err)
|
|
| 999 |
+ } |
|
| 1000 |
+ var etcdAlloc *etcdallocator.Etcd |
|
| 1001 |
+ uidAllocator := uidallocator.New(uidRange, func(max int, rangeSpec string) allocator.Interface {
|
|
| 1002 |
+ mem := allocator.NewContiguousAllocationMap(max, rangeSpec) |
|
| 1003 |
+ etcdAlloc = etcdallocator.NewEtcd(mem, "/ranges/uids", "uidallocation", c.EtcdHelper) |
|
| 1004 |
+ return etcdAlloc |
|
| 1005 |
+ }) |
|
| 1006 |
+ mcsRange, err := mcs.ParseRange("/2") // use two labels
|
|
| 1007 |
+ if err != nil {
|
|
| 1008 |
+ glog.Fatalf("Unable to describe MCS category range: %v", err)
|
|
| 1009 |
+ } |
|
| 1010 |
+ |
|
| 1011 |
+ kclient := c.PrivilegedLoopbackKubernetesClient |
|
| 1012 |
+ |
|
| 1013 |
+ repair := securitycontroller.NewRepair(time.Minute, kclient.Namespaces(), uidRange, etcdAlloc) |
|
| 1014 |
+ if err := repair.RunOnce(); err != nil {
|
|
| 1015 |
+ // TODO: v scary, may need to use direct etcd calls? |
|
| 1016 |
+ glog.Fatalf("Unable to initialize namespaces: %v", err)
|
|
| 1017 |
+ } |
|
| 1018 |
+ |
|
| 1019 |
+ factory := securitycontroller.AllocationFactory{
|
|
| 1020 |
+ UIDAllocator: uidAllocator, |
|
| 1021 |
+ MCSAllocator: securitycontroller.DefaultMCSAllocation(uidRange, mcsRange, 5), // provide 5 labels per namespace |
|
| 1022 |
+ Client: kclient.Namespaces(), |
|
| 1023 |
+ // TODO: reuse namespace cache |
|
| 1024 |
+ } |
|
| 1025 |
+ controller := factory.Create() |
|
| 1026 |
+ controller.Run() |
|
| 1027 |
+} |
|
| 1028 |
+ |
|
| 995 | 1029 |
// ensureCORSAllowedOrigins takes a string list of origins and attempts to covert them to CORS origin |
| 996 | 1030 |
// regexes, or exits if it cannot. |
| 997 | 1031 |
func (c *MasterConfig) ensureCORSAllowedOrigins() []*regexp.Regexp {
|
| ... | ... |
@@ -399,6 +399,7 @@ func StartMaster(openshiftMasterConfig *configapi.MasterConfig) error {
|
| 399 | 399 |
openshiftConfig.RunImageImportController() |
| 400 | 400 |
openshiftConfig.RunOriginNamespaceController() |
| 401 | 401 |
openshiftConfig.RunProjectAuthorizationCache() |
| 402 |
+ openshiftConfig.RunSecurityAllocationController() |
|
| 402 | 403 |
openshiftConfig.RunServiceAccountsController() |
| 403 | 404 |
openshiftConfig.RunServiceAccountTokensController() |
| 404 | 405 |
openshiftConfig.RunServiceAccountPullSecretsControllers() |
| 405 | 406 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,148 @@ |
| 0 |
+package controller |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ |
|
| 5 |
+ kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" |
|
| 6 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" |
|
| 7 |
+ kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" |
|
| 8 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/util" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/openshift/origin/pkg/security/mcs" |
|
| 11 |
+ "github.com/openshift/origin/pkg/security/uid" |
|
| 12 |
+ "github.com/openshift/origin/pkg/security/uidallocator" |
|
| 13 |
+) |
|
| 14 |
+ |
|
| 15 |
+const ( |
|
| 16 |
+ uidRangeAnnotation = "openshift.io/sa.scc.uid-range" |
|
| 17 |
+ mcsAnnotation = "openshift.io/sa.scc.mcs" |
|
| 18 |
+) |
|
| 19 |
+ |
|
| 20 |
+type MCSAllocationFunc func(uid.Block) *mcs.Label |
|
| 21 |
+ |
|
| 22 |
+// DefaultMCSAllocation returns a label from the MCS range that matches the offset |
|
| 23 |
+// within the overall range. blockSize must be a positive integer representing the |
|
| 24 |
+// number of labels to jump past in the category space (if 1, range == label, if 2 |
|
| 25 |
+// each range will have two labels). |
|
| 26 |
+func DefaultMCSAllocation(from *uid.Range, to *mcs.Range, blockSize int) MCSAllocationFunc {
|
|
| 27 |
+ return func(block uid.Block) *mcs.Label {
|
|
| 28 |
+ ok, offset := from.Offset(block) |
|
| 29 |
+ if !ok {
|
|
| 30 |
+ return nil |
|
| 31 |
+ } |
|
| 32 |
+ if blockSize > 0 {
|
|
| 33 |
+ offset = offset * uint32(blockSize) |
|
| 34 |
+ } |
|
| 35 |
+ label, _ := to.LabelAt(uint64(offset)) |
|
| 36 |
+ return label |
|
| 37 |
+ } |
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+type Allocation struct {
|
|
| 41 |
+ uid uidallocator.Interface |
|
| 42 |
+ mcs MCSAllocationFunc |
|
| 43 |
+ client kclient.NamespaceInterface |
|
| 44 |
+} |
|
| 45 |
+ |
|
| 46 |
+// retryCount is the number of times to retry on a conflict when updating a namespace |
|
| 47 |
+const retryCount = 2 |
|
| 48 |
+ |
|
| 49 |
+// Next processes a changed namespace and tries to allocate a uid range for it. If it is |
|
| 50 |
+// successful, an mcs label corresponding to the relative position of the range is also |
|
| 51 |
+// set. |
|
| 52 |
+func (c *Allocation) Next(ns *kapi.Namespace) error {
|
|
| 53 |
+ tx := &tx{}
|
|
| 54 |
+ defer tx.Rollback() |
|
| 55 |
+ |
|
| 56 |
+ if _, ok := ns.Annotations[uidRangeAnnotation]; ok {
|
|
| 57 |
+ return nil |
|
| 58 |
+ } |
|
| 59 |
+ |
|
| 60 |
+ if ns.Annotations == nil {
|
|
| 61 |
+ ns.Annotations = make(map[string]string) |
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ // do uid allocation |
|
| 65 |
+ block, err := c.uid.AllocateNext() |
|
| 66 |
+ if err != nil {
|
|
| 67 |
+ return err |
|
| 68 |
+ } |
|
| 69 |
+ tx.Add(func() error { return c.uid.Release(block) })
|
|
| 70 |
+ ns.Annotations[uidRangeAnnotation] = block.String() |
|
| 71 |
+ if _, ok := ns.Annotations[mcsAnnotation]; !ok {
|
|
| 72 |
+ if label := c.mcs(block); label != nil {
|
|
| 73 |
+ ns.Annotations[mcsAnnotation] = label.String() |
|
| 74 |
+ } |
|
| 75 |
+ } |
|
| 76 |
+ |
|
| 77 |
+ // TODO: could use a client.GuaranteedUpdate/Merge function |
|
| 78 |
+ for i := 0; i < retryCount; i++ {
|
|
| 79 |
+ _, err := c.client.Update(ns) |
|
| 80 |
+ if err == nil {
|
|
| 81 |
+ // commit and exit |
|
| 82 |
+ tx.Commit() |
|
| 83 |
+ return nil |
|
| 84 |
+ } |
|
| 85 |
+ |
|
| 86 |
+ if errors.IsNotFound(err) {
|
|
| 87 |
+ return nil |
|
| 88 |
+ } |
|
| 89 |
+ if !errors.IsConflict(err) {
|
|
| 90 |
+ return err |
|
| 91 |
+ } |
|
| 92 |
+ newNs, err := c.client.Get(ns.Name) |
|
| 93 |
+ if errors.IsNotFound(err) {
|
|
| 94 |
+ return nil |
|
| 95 |
+ } |
|
| 96 |
+ if err != nil {
|
|
| 97 |
+ return err |
|
| 98 |
+ } |
|
| 99 |
+ if changedAndSetAnnotations(ns, newNs) {
|
|
| 100 |
+ return nil |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ // try again |
|
| 104 |
+ if newNs.Annotations == nil {
|
|
| 105 |
+ newNs.Annotations = make(map[string]string) |
|
| 106 |
+ } |
|
| 107 |
+ newNs.Annotations[uidRangeAnnotation] = ns.Annotations[uidRangeAnnotation] |
|
| 108 |
+ newNs.Annotations[mcsAnnotation] = ns.Annotations[mcsAnnotation] |
|
| 109 |
+ ns = newNs |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ return fmt.Errorf("unable to allocate security info on %q after %d retries", ns.Name, retryCount)
|
|
| 113 |
+} |
|
| 114 |
+ |
|
| 115 |
+func changedAndSetAnnotations(old, ns *kapi.Namespace) bool {
|
|
| 116 |
+ if value, ok := ns.Annotations[uidRangeAnnotation]; ok && value != old.Annotations[uidRangeAnnotation] {
|
|
| 117 |
+ return true |
|
| 118 |
+ } |
|
| 119 |
+ if value, ok := ns.Annotations[mcsAnnotation]; ok && value != old.Annotations[mcsAnnotation] {
|
|
| 120 |
+ return true |
|
| 121 |
+ } |
|
| 122 |
+ return false |
|
| 123 |
+} |
|
| 124 |
+ |
|
| 125 |
+type tx struct {
|
|
| 126 |
+ rollback []func() error |
|
| 127 |
+} |
|
| 128 |
+ |
|
| 129 |
+func (tx *tx) Add(fn func() error) {
|
|
| 130 |
+ tx.rollback = append(tx.rollback, fn) |
|
| 131 |
+} |
|
| 132 |
+ |
|
| 133 |
+func (tx *tx) HasChanges() bool {
|
|
| 134 |
+ return len(tx.rollback) > 0 |
|
| 135 |
+} |
|
| 136 |
+ |
|
| 137 |
+func (tx *tx) Rollback() {
|
|
| 138 |
+ for _, fn := range tx.rollback {
|
|
| 139 |
+ if err := fn(); err != nil {
|
|
| 140 |
+ util.HandleError(fmt.Errorf("unable to undo tx: %v", err))
|
|
| 141 |
+ } |
|
| 142 |
+ } |
|
| 143 |
+} |
|
| 144 |
+ |
|
| 145 |
+func (tx *tx) Commit() {
|
|
| 146 |
+ tx.rollback = nil |
|
| 147 |
+} |
| 0 | 148 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,111 @@ |
| 0 |
+package controller |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "strings" |
|
| 5 |
+ "testing" |
|
| 6 |
+ |
|
| 7 |
+ kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" |
|
| 8 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" |
|
| 9 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" |
|
| 10 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" |
|
| 11 |
+ |
|
| 12 |
+ "github.com/openshift/origin/pkg/security/mcs" |
|
| 13 |
+ "github.com/openshift/origin/pkg/security/uid" |
|
| 14 |
+ "github.com/openshift/origin/pkg/security/uidallocator" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+func TestController(t *testing.T) {
|
|
| 18 |
+ var action testclient.FakeAction |
|
| 19 |
+ client := &testclient.Fake{
|
|
| 20 |
+ ReactFn: func(a testclient.FakeAction) (runtime.Object, error) {
|
|
| 21 |
+ action = a |
|
| 22 |
+ return (*kapi.Namespace)(nil), nil |
|
| 23 |
+ }, |
|
| 24 |
+ } |
|
| 25 |
+ uidr, _ := uid.NewRange(10, 20, 2) |
|
| 26 |
+ mcsr, _ := mcs.NewRange("s0:", 10, 2)
|
|
| 27 |
+ uida := uidallocator.NewInMemory(uidr) |
|
| 28 |
+ c := Allocation{
|
|
| 29 |
+ uid: uida, |
|
| 30 |
+ mcs: DefaultMCSAllocation(uidr, mcsr, 5), |
|
| 31 |
+ client: client.Namespaces(), |
|
| 32 |
+ } |
|
| 33 |
+ |
|
| 34 |
+ err := c.Next(&kapi.Namespace{ObjectMeta: kapi.ObjectMeta{Name: "test"}})
|
|
| 35 |
+ if err != nil {
|
|
| 36 |
+ t.Fatal(err) |
|
| 37 |
+ } |
|
| 38 |
+ |
|
| 39 |
+ got := action.Value.(*kapi.Namespace) |
|
| 40 |
+ if got.Annotations[uidRangeAnnotation] != "10/2" {
|
|
| 41 |
+ t.Errorf("unexpected annotation: %#v", got)
|
|
| 42 |
+ } |
|
| 43 |
+ if got.Annotations[mcsAnnotation] != "s0:c1,c0" {
|
|
| 44 |
+ t.Errorf("unexpected annotation: %#v", got)
|
|
| 45 |
+ } |
|
| 46 |
+ if !uida.Has(uid.Block{Start: 10, End: 11}) {
|
|
| 47 |
+ t.Errorf("did not allocate uid: %#v", uida)
|
|
| 48 |
+ } |
|
| 49 |
+} |
|
| 50 |
+ |
|
| 51 |
+func TestControllerError(t *testing.T) {
|
|
| 52 |
+ testCases := map[string]struct {
|
|
| 53 |
+ err func() error |
|
| 54 |
+ errFn func(err error) bool |
|
| 55 |
+ reactFn testclient.ReactionFunc |
|
| 56 |
+ actions int |
|
| 57 |
+ }{
|
|
| 58 |
+ "not found": {
|
|
| 59 |
+ err: func() error { return errors.NewNotFound("namespace", "test") },
|
|
| 60 |
+ errFn: func(err error) bool { return err == nil },
|
|
| 61 |
+ actions: 1, |
|
| 62 |
+ }, |
|
| 63 |
+ "unknown": {
|
|
| 64 |
+ err: func() error { return fmt.Errorf("unknown") },
|
|
| 65 |
+ errFn: func(err error) bool { return err.Error() == "unknown" },
|
|
| 66 |
+ actions: 1, |
|
| 67 |
+ }, |
|
| 68 |
+ "conflict": {
|
|
| 69 |
+ actions: 4, |
|
| 70 |
+ reactFn: func(a testclient.FakeAction) (runtime.Object, error) {
|
|
| 71 |
+ if a.Action == "get-namespace" {
|
|
| 72 |
+ return &kapi.Namespace{ObjectMeta: kapi.ObjectMeta{Name: "test"}}, nil
|
|
| 73 |
+ } |
|
| 74 |
+ return (*kapi.Namespace)(nil), errors.NewConflict("namespace", "test", fmt.Errorf("test conflict"))
|
|
| 75 |
+ }, |
|
| 76 |
+ errFn: func(err error) bool {
|
|
| 77 |
+ return err != nil && strings.Contains(err.Error(), "unable to allocate security info") |
|
| 78 |
+ }, |
|
| 79 |
+ }, |
|
| 80 |
+ } |
|
| 81 |
+ |
|
| 82 |
+ for s, testCase := range testCases {
|
|
| 83 |
+ client := &testclient.Fake{ReactFn: testCase.reactFn}
|
|
| 84 |
+ if client.ReactFn == nil {
|
|
| 85 |
+ client.ReactFn = func(a testclient.FakeAction) (runtime.Object, error) {
|
|
| 86 |
+ return (*kapi.Namespace)(nil), testCase.err() |
|
| 87 |
+ } |
|
| 88 |
+ } |
|
| 89 |
+ uidr, _ := uid.NewRange(10, 19, 2) |
|
| 90 |
+ mcsr, _ := mcs.NewRange("s0:", 10, 2)
|
|
| 91 |
+ uida := uidallocator.NewInMemory(uidr) |
|
| 92 |
+ c := Allocation{
|
|
| 93 |
+ uid: uida, |
|
| 94 |
+ mcs: DefaultMCSAllocation(uidr, mcsr, 5), |
|
| 95 |
+ client: client.Namespaces(), |
|
| 96 |
+ } |
|
| 97 |
+ |
|
| 98 |
+ err := c.Next(&kapi.Namespace{ObjectMeta: kapi.ObjectMeta{Name: "test"}})
|
|
| 99 |
+ if !testCase.errFn(err) {
|
|
| 100 |
+ t.Errorf("%s: unexpected error: %v", s, err)
|
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ if len(client.Actions) != testCase.actions {
|
|
| 104 |
+ t.Errorf("%s: expected %d actions: %v", s, testCase.actions, client.Actions)
|
|
| 105 |
+ } |
|
| 106 |
+ if uida.Free() != 5 {
|
|
| 107 |
+ t.Errorf("%s: should not have allocated uid: %d/%d", s, uida.Free(), uidr.Size())
|
|
| 108 |
+ } |
|
| 109 |
+ } |
|
| 110 |
+} |
| 0 | 111 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,68 @@ |
| 0 |
+package controller |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "time" |
|
| 4 |
+ |
|
| 5 |
+ kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" |
|
| 6 |
+ kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" |
|
| 7 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/client/cache" |
|
| 8 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" |
|
| 9 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" |
|
| 10 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" |
|
| 11 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/util" |
|
| 12 |
+ kutil "github.com/GoogleCloudPlatform/kubernetes/pkg/util" |
|
| 13 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" |
|
| 14 |
+ |
|
| 15 |
+ "github.com/openshift/origin/pkg/controller" |
|
| 16 |
+ "github.com/openshift/origin/pkg/security/uidallocator" |
|
| 17 |
+) |
|
| 18 |
+ |
|
| 19 |
+// AllocationFactory can create an Allocation controller. |
|
| 20 |
+type AllocationFactory struct {
|
|
| 21 |
+ UIDAllocator uidallocator.Interface |
|
| 22 |
+ MCSAllocator MCSAllocationFunc |
|
| 23 |
+ Client kclient.NamespaceInterface |
|
| 24 |
+ // Queue may be a FIFO queue of namespaces. If nil, will be initialized using |
|
| 25 |
+ // the client. |
|
| 26 |
+ Queue controller.ReQueue |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+// Create creates a Allocation. |
|
| 30 |
+func (f *AllocationFactory) Create() controller.RunnableController {
|
|
| 31 |
+ if f.Queue == nil {
|
|
| 32 |
+ lw := &cache.ListWatch{
|
|
| 33 |
+ ListFunc: func() (runtime.Object, error) {
|
|
| 34 |
+ return f.Client.List(labels.Everything(), fields.Everything()) |
|
| 35 |
+ }, |
|
| 36 |
+ WatchFunc: func(resourceVersion string) (watch.Interface, error) {
|
|
| 37 |
+ return f.Client.Watch(labels.Everything(), fields.Everything(), resourceVersion) |
|
| 38 |
+ }, |
|
| 39 |
+ } |
|
| 40 |
+ q := cache.NewFIFO(cache.MetaNamespaceKeyFunc) |
|
| 41 |
+ cache.NewReflector(lw, &kapi.Namespace{}, q, 10*time.Minute).Run()
|
|
| 42 |
+ f.Queue = q |
|
| 43 |
+ } |
|
| 44 |
+ |
|
| 45 |
+ c := &Allocation{
|
|
| 46 |
+ uid: f.UIDAllocator, |
|
| 47 |
+ mcs: f.MCSAllocator, |
|
| 48 |
+ client: f.Client, |
|
| 49 |
+ } |
|
| 50 |
+ |
|
| 51 |
+ return &controller.RetryController{
|
|
| 52 |
+ Queue: f.Queue, |
|
| 53 |
+ RetryManager: controller.NewQueueRetryManager( |
|
| 54 |
+ f.Queue, |
|
| 55 |
+ cache.MetaNamespaceKeyFunc, |
|
| 56 |
+ func(obj interface{}, err error, retries controller.Retry) bool {
|
|
| 57 |
+ util.HandleError(err) |
|
| 58 |
+ return retries.Count < 5 |
|
| 59 |
+ }, |
|
| 60 |
+ kutil.NewTokenBucketRateLimiter(1, 10), |
|
| 61 |
+ ), |
|
| 62 |
+ Handle: func(obj interface{}) error {
|
|
| 63 |
+ r := obj.(*kapi.Namespace) |
|
| 64 |
+ return c.Next(r) |
|
| 65 |
+ }, |
|
| 66 |
+ } |
|
| 67 |
+} |
| 0 | 68 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,100 @@ |
| 0 |
+package controller |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "fmt" |
|
| 4 |
+ "time" |
|
| 5 |
+ |
|
| 6 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/client" |
|
| 7 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" |
|
| 8 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" |
|
| 9 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service" |
|
| 10 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/util" |
|
| 11 |
+ |
|
| 12 |
+ "github.com/openshift/origin/pkg/security/uid" |
|
| 13 |
+ "github.com/openshift/origin/pkg/security/uidallocator" |
|
| 14 |
+) |
|
| 15 |
+ |
|
| 16 |
+// Repair is a controller loop that periodically examines all UID allocations |
|
| 17 |
+// and logs any errors, and then sets the compacted and accurate list of both |
|
| 18 |
+// |
|
| 19 |
+// Can be run at infrequent intervals, and is best performed on startup of the master. |
|
| 20 |
+// Is level driven and idempotent - all claimed UIDs will be updated into the allocator |
|
| 21 |
+// map at the end of a single execution loop if no race is encountered. |
|
| 22 |
+// |
|
| 23 |
+type Repair struct {
|
|
| 24 |
+ interval time.Duration |
|
| 25 |
+ client client.NamespaceInterface |
|
| 26 |
+ alloc service.RangeRegistry |
|
| 27 |
+ uidRange *uid.Range |
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+// NewRepair creates a controller that periodically ensures that all UIDs labels that are allocated in the cluster |
|
| 31 |
+// are claimed. |
|
| 32 |
+func NewRepair(interval time.Duration, client client.NamespaceInterface, uidRange *uid.Range, alloc service.RangeRegistry) *Repair {
|
|
| 33 |
+ return &Repair{
|
|
| 34 |
+ interval: interval, |
|
| 35 |
+ client: client, |
|
| 36 |
+ uidRange: uidRange, |
|
| 37 |
+ alloc: alloc, |
|
| 38 |
+ } |
|
| 39 |
+} |
|
| 40 |
+ |
|
| 41 |
+// RunUntil starts the controller until the provided ch is closed. |
|
| 42 |
+func (c *Repair) RunUntil(ch chan struct{}) {
|
|
| 43 |
+ util.Until(func() {
|
|
| 44 |
+ if err := c.RunOnce(); err != nil {
|
|
| 45 |
+ util.HandleError(err) |
|
| 46 |
+ } |
|
| 47 |
+ }, c.interval, ch) |
|
| 48 |
+} |
|
| 49 |
+ |
|
| 50 |
+// RunOnce verifies the state of allocations and returns an error if an unrecoverable problem occurs. |
|
| 51 |
+func (c *Repair) RunOnce() error {
|
|
| 52 |
+ // TODO: (per smarterclayton) if Get() or List() is a weak consistency read, |
|
| 53 |
+ // or if they are executed against different leaders, |
|
| 54 |
+ // the ordering guarantee required to ensure no item is allocated twice is violated. |
|
| 55 |
+ // List must return a ResourceVersion higher than the etcd index Get, |
|
| 56 |
+ // and the release code must not release items that have allocated but not yet been created |
|
| 57 |
+ // See #8295 |
|
| 58 |
+ |
|
| 59 |
+ latest, err := c.alloc.Get() |
|
| 60 |
+ if err != nil {
|
|
| 61 |
+ return fmt.Errorf("unable to refresh the security allocation UID blocks: %v", err)
|
|
| 62 |
+ } |
|
| 63 |
+ |
|
| 64 |
+ list, err := c.client.List(labels.Everything(), fields.Everything()) |
|
| 65 |
+ if err != nil {
|
|
| 66 |
+ return fmt.Errorf("unable to refresh the security allocation UID blocks: %v", err)
|
|
| 67 |
+ } |
|
| 68 |
+ |
|
| 69 |
+ uids := uidallocator.NewInMemory(c.uidRange) |
|
| 70 |
+ |
|
| 71 |
+ for _, ns := range list.Items {
|
|
| 72 |
+ value, ok := ns.Annotations[uidRangeAnnotation] |
|
| 73 |
+ if !ok {
|
|
| 74 |
+ continue |
|
| 75 |
+ } |
|
| 76 |
+ block, err := uid.ParseBlock(value) |
|
| 77 |
+ if err != nil {
|
|
| 78 |
+ continue |
|
| 79 |
+ } |
|
| 80 |
+ switch err := uids.Allocate(block); err {
|
|
| 81 |
+ case nil: |
|
| 82 |
+ case uidallocator.ErrFull: |
|
| 83 |
+ // TODO: send event |
|
| 84 |
+ return fmt.Errorf("the UID range %s is full; you must widen the range in order to allocate more UIDs", c.uidRange)
|
|
| 85 |
+ default: |
|
| 86 |
+ return fmt.Errorf("unable to allocate UID block %s for namespace %s due to an unknown error, exiting: %v", block, ns.Name, err)
|
|
| 87 |
+ } |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ err = uids.Snapshot(latest) |
|
| 91 |
+ if err != nil {
|
|
| 92 |
+ return fmt.Errorf("unable to persist the updated namespace UID allocations: %v", err)
|
|
| 93 |
+ } |
|
| 94 |
+ |
|
| 95 |
+ if err := c.alloc.CreateOrUpdate(latest); err != nil {
|
|
| 96 |
+ return fmt.Errorf("unable to persist the updated namespace UID allocations: %v", err)
|
|
| 97 |
+ } |
|
| 98 |
+ return nil |
|
| 99 |
+} |
| 0 | 100 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,63 @@ |
| 0 |
+package controller |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "testing" |
|
| 4 |
+ "time" |
|
| 5 |
+ |
|
| 6 |
+ kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" |
|
| 7 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" |
|
| 8 |
+ "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" |
|
| 9 |
+ |
|
| 10 |
+ "github.com/openshift/origin/pkg/security/uid" |
|
| 11 |
+) |
|
| 12 |
+ |
|
| 13 |
+type fakeRange struct {
|
|
| 14 |
+ Err error |
|
| 15 |
+ Range *kapi.RangeAllocation |
|
| 16 |
+ Updated *kapi.RangeAllocation |
|
| 17 |
+ UpdateErr error |
|
| 18 |
+} |
|
| 19 |
+ |
|
| 20 |
+func (r *fakeRange) Get() (*kapi.RangeAllocation, error) {
|
|
| 21 |
+ return r.Range, r.Err |
|
| 22 |
+} |
|
| 23 |
+ |
|
| 24 |
+func (r *fakeRange) CreateOrUpdate(update *kapi.RangeAllocation) error {
|
|
| 25 |
+ r.Updated = update |
|
| 26 |
+ return r.UpdateErr |
|
| 27 |
+} |
|
| 28 |
+ |
|
| 29 |
+func TestRepair(t *testing.T) {
|
|
| 30 |
+ var action testclient.FakeAction |
|
| 31 |
+ client := &testclient.Fake{
|
|
| 32 |
+ ReactFn: func(a testclient.FakeAction) (runtime.Object, error) {
|
|
| 33 |
+ action = a |
|
| 34 |
+ list := &kapi.NamespaceList{
|
|
| 35 |
+ Items: []kapi.Namespace{
|
|
| 36 |
+ {ObjectMeta: kapi.ObjectMeta{Name: "default"}},
|
|
| 37 |
+ }, |
|
| 38 |
+ } |
|
| 39 |
+ return list, nil |
|
| 40 |
+ }, |
|
| 41 |
+ } |
|
| 42 |
+ alloc := &fakeRange{
|
|
| 43 |
+ Range: &kapi.RangeAllocation{},
|
|
| 44 |
+ } |
|
| 45 |
+ |
|
| 46 |
+ uidr, _ := uid.NewRange(10, 20, 2) |
|
| 47 |
+ repair := NewRepair(0*time.Second, client.Namespaces(), uidr, alloc) |
|
| 48 |
+ |
|
| 49 |
+ err := repair.RunOnce() |
|
| 50 |
+ if err != nil {
|
|
| 51 |
+ t.Fatal(err) |
|
| 52 |
+ } |
|
| 53 |
+ if alloc.Updated == nil {
|
|
| 54 |
+ t.Fatalf("did not store range: %#v", alloc)
|
|
| 55 |
+ } |
|
| 56 |
+ if alloc.Updated.Range != "10-20/2" {
|
|
| 57 |
+ t.Errorf("didn't store range properly: %#v", alloc.Updated)
|
|
| 58 |
+ } |
|
| 59 |
+ if len(alloc.Updated.Data) != 0 {
|
|
| 60 |
+ t.Errorf("data wasn't empty: %#v", alloc.Updated)
|
|
| 61 |
+ } |
|
| 62 |
+} |
| ... | ... |
@@ -41,6 +41,11 @@ func New(r *mcs.Range, factory allocator.AllocatorFactory) *Allocator {
|
| 41 | 41 |
} |
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 |
+// NewInMemory creates an in-memory Allocator |
|
| 45 |
+func NewInMemory(r *mcs.Range) *Allocator {
|
|
| 46 |
+ return New(r, allocator.NewContiguousAllocationInterface) |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 44 | 49 |
// Free returns the count of port left in the range. |
| 45 | 50 |
func (r *Allocator) Free() int {
|
| 46 | 51 |
return r.alloc.Free() |
| ... | ... |
@@ -2,6 +2,7 @@ package uid |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
+ "strings" |
|
| 5 | 6 |
) |
| 6 | 7 |
|
| 7 | 8 |
type Block struct {
|
| ... | ... |
@@ -9,7 +10,35 @@ type Block struct {
|
| 9 | 9 |
End uint32 |
| 10 | 10 |
} |
| 11 | 11 |
|
| 12 |
+func ParseBlock(in string) (Block, error) {
|
|
| 13 |
+ if strings.Contains(in, "/") {
|
|
| 14 |
+ var start, size uint32 |
|
| 15 |
+ n, err := fmt.Sscanf(in, "%d/%d", &start, &size) |
|
| 16 |
+ if err != nil {
|
|
| 17 |
+ return Block{}, err
|
|
| 18 |
+ } |
|
| 19 |
+ if n != 2 {
|
|
| 20 |
+ return Block{}, fmt.Errorf("block not in the format \"<start>/<size>\"")
|
|
| 21 |
+ } |
|
| 22 |
+ return Block{Start: start, End: start + size - 1}, nil
|
|
| 23 |
+ } |
|
| 24 |
+ |
|
| 25 |
+ var start, end uint32 |
|
| 26 |
+ n, err := fmt.Sscanf(in, "%d-%d", &start, &end) |
|
| 27 |
+ if err != nil {
|
|
| 28 |
+ return Block{}, err
|
|
| 29 |
+ } |
|
| 30 |
+ if n != 2 {
|
|
| 31 |
+ return Block{}, fmt.Errorf("block not in the format \"<start>-<end>\"")
|
|
| 32 |
+ } |
|
| 33 |
+ return Block{Start: start, End: end}, nil
|
|
| 34 |
+} |
|
| 35 |
+ |
|
| 12 | 36 |
func (b Block) String() string {
|
| 37 |
+ return fmt.Sprintf("%d/%d", b.Start, b.Size())
|
|
| 38 |
+} |
|
| 39 |
+ |
|
| 40 |
+func (b Block) RangeString() string {
|
|
| 13 | 41 |
return fmt.Sprintf("%d-%d", b.Start, b.End)
|
| 14 | 42 |
} |
| 15 | 43 |
|
| ... | ... |
@@ -55,7 +84,7 @@ func (r *Range) Size() uint32 {
|
| 55 | 55 |
} |
| 56 | 56 |
|
| 57 | 57 |
func (r *Range) String() string {
|
| 58 |
- return fmt.Sprintf("%s/%d", r.block, r.size)
|
|
| 58 |
+ return fmt.Sprintf("%s/%d", r.block.RangeString(), r.size)
|
|
| 59 | 59 |
} |
| 60 | 60 |
|
| 61 | 61 |
func (r *Range) BlockAt(offset uint32) (Block, bool) {
|
| ... | ... |
@@ -72,6 +72,20 @@ func TestParseRange(t *testing.T) {
|
| 72 | 72 |
} |
| 73 | 73 |
} |
| 74 | 74 |
|
| 75 |
+func TestBlock(t *testing.T) {
|
|
| 76 |
+ b := Block{Start: 100, End: 109}
|
|
| 77 |
+ if b.String() != "100/10" {
|
|
| 78 |
+ t.Errorf("unexpected block string: %s", b.String())
|
|
| 79 |
+ } |
|
| 80 |
+ b, err := ParseBlock("100-109")
|
|
| 81 |
+ if err != nil {
|
|
| 82 |
+ t.Fatal(err) |
|
| 83 |
+ } |
|
| 84 |
+ if b.String() != "100/10" {
|
|
| 85 |
+ t.Errorf("unexpected block string: %s", b.String())
|
|
| 86 |
+ } |
|
| 87 |
+} |
|
| 88 |
+ |
|
| 75 | 89 |
func TestOffset(t *testing.T) {
|
| 76 | 90 |
testCases := map[string]struct {
|
| 77 | 91 |
r Range |
| ... | ... |
@@ -41,6 +41,11 @@ func New(r *uid.Range, factory allocator.AllocatorFactory) *Allocator {
|
| 41 | 41 |
} |
| 42 | 42 |
} |
| 43 | 43 |
|
| 44 |
+// NewInMemory creates an in-memory Allocator |
|
| 45 |
+func NewInMemory(r *uid.Range) *Allocator {
|
|
| 46 |
+ return New(r, allocator.NewContiguousAllocationInterface) |
|
| 47 |
+} |
|
| 48 |
+ |
|
| 44 | 49 |
// Free returns the count of port left in the range. |
| 45 | 50 |
func (r *Allocator) Free() int {
|
| 46 | 51 |
return r.alloc.Free() |