From 6127bbeb4935deeddef6df088720d5b4e588e8b9 Mon Sep 17 00:00:00 2001 From: Zsolt Zadory Date: Tue, 8 Jan 2019 18:30:48 +0200 Subject: [PATCH] Add chart/manifest weight based ordering Signed-off-by: Zsolt Zadory --- _proto/hapi/chart/metadata.proto | 5 +- cmd/rudder/rudder.go | 18 ++-- docs/chart_best_practices/weights.md | 23 ++++ docs/charts.md | 1 + pkg/chartutil/requirements.go | 2 + pkg/kube/client.go | 33 ++++-- pkg/kube/client_test.go | 4 + pkg/manifest/operators.go | 62 +++++++++++ pkg/manifest/splitter.go | 4 +- pkg/manifest/types.go | 24 ++++- pkg/proto/hapi/chart/metadata.pb.go | 78 ++++++++------ pkg/releasetesting/test_suite.go | 3 +- pkg/releaseutil/manifest.go | 74 ++++++++++--- pkg/tiller/environment/environment.go | 8 ++ pkg/tiller/environment/environment_test.go | 3 + pkg/tiller/hooks.go | 70 +++++++++++-- pkg/tiller/hooks_test.go | 9 +- pkg/tiller/kind_sorter.go | 8 +- pkg/tiller/kind_sorter_test.go | 102 +++++++++--------- pkg/tiller/release_install_test.go | 10 +- pkg/tiller/release_modules.go | 116 +++++++++++++++++---- pkg/tiller/release_server.go | 3 +- pkg/tiller/release_server_test.go | 3 + pkg/tiller/release_update_test.go | 2 +- pkg/tiller/weight_sorter.go | 56 ++++++++++ pkg/tiller/weight_sorter_test.go | 108 +++++++++++++++++++ 26 files changed, 664 insertions(+), 165 deletions(-) create mode 100644 docs/chart_best_practices/weights.md create mode 100644 pkg/manifest/operators.go create mode 100644 pkg/tiller/weight_sorter.go create mode 100644 pkg/tiller/weight_sorter_test.go diff --git a/_proto/hapi/chart/metadata.proto b/_proto/hapi/chart/metadata.proto index 0ac695ce2..bf1d72766 100644 --- a/_proto/hapi/chart/metadata.proto +++ b/_proto/hapi/chart/metadata.proto @@ -89,5 +89,8 @@ message Metadata { map annotations = 16; // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. - string kubeVersion = 17; + string kubeVersion = 17; + + // Deployment weight of this chart + uint32 weight = 18; } diff --git a/cmd/rudder/rudder.go b/cmd/rudder/rudder.go index d68daf453..38b531f52 100644 --- a/cmd/rudder/rudder.go +++ b/cmd/rudder/rudder.go @@ -131,14 +131,16 @@ func (r *ReleaseModuleServiceServer) RollbackRelease(ctx context.Context, in *ru grpclog.Print("rollback") c := bytes.NewBufferString(in.Current.Manifest) t := bytes.NewBufferString(in.Target.Manifest) - err := kubeClient.UpdateWithOptions(in.Target.Namespace, c, t, kube.UpdateOptions{ + if err := kubeClient.UpdateWithOptions(in.Target.Namespace, c, t, kube.UpdateOptions{ Force: in.Force, Recreate: in.Recreate, Timeout: in.Timeout, ShouldWait: in.Wait, CleanupOnFail: in.CleanupOnFail, - }) - return &rudderAPI.RollbackReleaseResponse{}, err + }); err != nil { + return &rudderAPI.RollbackReleaseResponse{}, err + } + return &rudderAPI.RollbackReleaseResponse{}, nil } // UpgradeRelease upgrades manifests using kubernetes client @@ -146,15 +148,17 @@ func (r *ReleaseModuleServiceServer) UpgradeRelease(ctx context.Context, in *rud grpclog.Print("upgrade") c := bytes.NewBufferString(in.Current.Manifest) t := bytes.NewBufferString(in.Target.Manifest) - err := kubeClient.UpdateWithOptions(in.Target.Namespace, c, t, kube.UpdateOptions{ + if err := kubeClient.UpdateWithOptions(in.Target.Namespace, c, t, kube.UpdateOptions{ Force: in.Force, Recreate: in.Recreate, Timeout: in.Timeout, ShouldWait: in.Wait, CleanupOnFail: in.CleanupOnFail, - }) - // upgrade response object should be changed to include status - return &rudderAPI.UpgradeReleaseResponse{}, err + }); err != nil { + // upgrade response object should be changed to include status + return &rudderAPI.UpgradeReleaseResponse{}, err + } + return &rudderAPI.UpgradeReleaseResponse{}, nil } // ReleaseStatus retrieves release status diff --git a/docs/chart_best_practices/weights.md b/docs/chart_best_practices/weights.md new file mode 100644 index 000000000..2d149e5a4 --- /dev/null +++ b/docs/chart_best_practices/weights.md @@ -0,0 +1,23 @@ +# Chart and template weights + +This part of the Best Practices Guide discusses how to manipulate the creation order of kubernetes resources + +The weight of an object can be given at two levels: + +- Chart: It can be defined in `Chart.yaml`. All templates directly defined in this chart is affected. It takes precedence over template weight. +- Template: A `helm.sh/order-weight` annotation shall be declared in the uppermost metadata section of a kubernetes object. + +## Precedence + +Every object will get a tuple value of (chart weight, object weight). Default is 0 for both. +Chart weight takes precedence over individual objects. Higher weight means it will be handled earlier. + +## Scope + +Weights have a global scope, so if subchart has higher weight then it will be handled sooner regardless of the inclusion order. + +## How it works + +Without the `--wait` flag it works like kind ordering - all objects will be sent to apiserver in one big manifest and objects are sorted accordingly (sortByWeight(sortByKind(templates))). + +With the `--wait` flag equally weighted objects are grouped together, and these groups are then installed one-by-one. Helm waits for each group's resources individually for the given `--timeout` period. diff --git a/docs/charts.md b/docs/charts.md index 62b77647d..edecf2179 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -60,6 +60,7 @@ icon: A URL to an SVG or PNG image to be used as an icon (optional). appVersion: The version of the app that this contains (optional). This needn't be SemVer. deprecated: Whether this chart is deprecated (optional, boolean) tillerVersion: The version of Tiller that this chart requires. This should be expressed as a SemVer range: ">2.0.0" (optional) +weight: A number (>0) that represents the precedence of objects in the chart. Heavier objects will be installed sooner. (optional) ``` If you are familiar with the `Chart.yaml` file format for Helm Classic, you will diff --git a/pkg/chartutil/requirements.go b/pkg/chartutil/requirements.go index ac2649d7c..922df36de 100644 --- a/pkg/chartutil/requirements.go +++ b/pkg/chartutil/requirements.go @@ -68,6 +68,8 @@ type Dependency struct { ImportValues []interface{} `json:"import-values,omitempty"` // Alias usable alias to be used for the chart Alias string `json:"alias,omitempty"` + // Deployment weight of this chart + Weight uint32 `json:"weight,omitempty"` } // ErrNoRequirementsFile to detect error condition diff --git a/pkg/kube/client.go b/pkg/kube/client.go index e61d526a7..5b7830dfb 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -423,6 +423,28 @@ func (c *Client) UpdateWithOptions(namespace string, originalReader, targetReade return fmt.Errorf(strings.Join(append(updateErrors, cleanupErrors...), " && ")) } + if opts.ShouldWait { + return c.waitForResources(time.Duration(opts.Timeout)*time.Second, target) + } + return nil +} + +// RemoveDiff deletes resources from the current configuration that are +// not present in the target configuration +// +// Namespace will set the namespace. +func (c *Client) RemoveDiff(namespace string, originalReader, targetReader io.Reader) error { + original, err := c.BuildUnstructured(namespace, originalReader) + if err != nil { + return fmt.Errorf("failed decoding reader into objects: %s", err) + } + + c.Log("building resources from updated manifest") + target, err := c.BuildUnstructured(namespace, targetReader) + if err != nil { + return fmt.Errorf("failed decoding reader into objects: %s", err) + } + for _, info := range original.Difference(target) { c.Log("Deleting %q in %s...", info.Name, info.Namespace) @@ -443,17 +465,6 @@ func (c *Client) UpdateWithOptions(namespace string, originalReader, targetReade c.Log("Failed to delete %q, err: %s", info.Name, err) } } - if opts.ShouldWait { - err := c.waitForResources(time.Duration(opts.Timeout)*time.Second, target) - - if opts.CleanupOnFail && err != nil { - c.Log("Cleanup on fail enabled: cleaning up newly created resources due to wait failure during update") - cleanupErrors = c.cleanup(newlyCreatedResources) - return fmt.Errorf(strings.Join(append([]string{err.Error()}, cleanupErrors...), " && ")) - } - - return err - } return nil } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index d33b4b9d9..d22c4bf44 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -203,6 +203,10 @@ func TestUpdate(t *testing.T) { if err := c.Update(v1.NamespaceDefault, objBody(&listA), objBody(&listB), false, false, 0, false); err != nil { t.Fatal(err) } + + if err := c.RemoveDiff(v1.NamespaceDefault, objBody(&listA), objBody(&listB)); err != nil { + t.Fatal(err) + } // TODO: Find a way to test methods that use Client Set // Test with a wait // if err := c.Update("test", objBody(&listB), objBody(&listC), false, 300, true); err != nil { diff --git a/pkg/manifest/operators.go b/pkg/manifest/operators.go new file mode 100644 index 000000000..a4bcaaeee --- /dev/null +++ b/pkg/manifest/operators.go @@ -0,0 +1,62 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package manifest + +// Equalto implements = operator for weights +// +// Weights are equal if both Chart and Manifest attributes are equal +func (mw *Weight) Equalto(other *Weight) bool { + if mw == nil || other == nil { + return false + } + + if mw.Chart == other.Chart && mw.Manifest == other.Manifest { + return true + } + + return false +} + +// LessThan implements < operator for weights +// +// Precedence is Weight.Chart > Weight.Manifest +func (mw *Weight) LessThan(other *Weight) bool { + if mw == nil || other == nil { + return false + } + + if mw.Chart == other.Chart { + return mw.Manifest < other.Manifest + } + + return mw.Chart < other.Chart +} + +// GreaterThan implements > operator for weights +// +// Precedence is Weight.Chart > Weight.Manifest +func (mw *Weight) GreaterThan(other *Weight) bool { + if mw == nil || other == nil { + return false + } + + if mw.Chart == other.Chart { + return mw.Manifest > other.Manifest + } + + return mw.Chart > other.Chart +} diff --git a/pkg/manifest/splitter.go b/pkg/manifest/splitter.go index 7081e7aa7..2f6e2e328 100644 --- a/pkg/manifest/splitter.go +++ b/pkg/manifest/splitter.go @@ -19,8 +19,6 @@ package manifest import ( "regexp" "strings" - - "k8s.io/helm/pkg/releaseutil" ) var ( @@ -38,7 +36,7 @@ func SplitManifests(templates map[string]string) []Manifest { if len(match) == 2 { h = strings.TrimSpace(match[1]) } - m := Manifest{Name: k, Content: v, Head: &releaseutil.SimpleHead{Kind: h}} + m := Manifest{Name: k, Content: v, Head: &SimpleHead{Kind: h}} listManifests = append(listManifests, m) } diff --git a/pkg/manifest/types.go b/pkg/manifest/types.go index 4c748c9e5..a5166d232 100644 --- a/pkg/manifest/types.go +++ b/pkg/manifest/types.go @@ -16,13 +16,29 @@ limitations under the License. package manifest -import ( - "k8s.io/helm/pkg/releaseutil" -) +// ManifestOrderWeight is the label name for a manifest +const ManifestOrderWeight = "helm.sh/order-weight" + +// Weight represents the deployment order of a manifest +type Weight struct { + Chart uint32 + Manifest uint32 +} + +// SimpleHead defines what the structure of the head of a manifest file +type SimpleHead struct { + Version string `json:"apiVersion"` + Kind string `json:"kind,omitempty"` + Metadata *struct { + Name string `json:"name"` + Annotations map[string]string `json:"annotations"` + } `json:"metadata,omitempty"` +} // Manifest represents a manifest file, which has a name and some content. type Manifest struct { Name string Content string - Head *releaseutil.SimpleHead + Head *SimpleHead + Weight *Weight } diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index ebf59fd9f..a4ba7f528 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -38,7 +38,7 @@ func (x Metadata_Engine) String() string { return proto.EnumName(Metadata_Engine_name, int32(x)) } func (Metadata_Engine) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_metadata_d6c714c73a051dcb, []int{1, 0} + return fileDescriptor_metadata_a633fda7c87700ff, []int{1, 0} } // Maintainer describes a Chart maintainer. @@ -58,7 +58,7 @@ func (m *Maintainer) Reset() { *m = Maintainer{} } func (m *Maintainer) String() string { return proto.CompactTextString(m) } func (*Maintainer) ProtoMessage() {} func (*Maintainer) Descriptor() ([]byte, []int) { - return fileDescriptor_metadata_d6c714c73a051dcb, []int{0} + return fileDescriptor_metadata_a633fda7c87700ff, []int{0} } func (m *Maintainer) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Maintainer.Unmarshal(m, b) @@ -138,7 +138,9 @@ type Metadata struct { // made available for inspection by other applications. Annotations map[string]string `protobuf:"bytes,16,rep,name=annotations,proto3" json:"annotations,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. - KubeVersion string `protobuf:"bytes,17,opt,name=kubeVersion,proto3" json:"kubeVersion,omitempty"` + KubeVersion string `protobuf:"bytes,17,opt,name=kubeVersion,proto3" json:"kubeVersion,omitempty"` + // Deployment weight of this chart + Weight uint32 `protobuf:"varint,18,opt,name=weight,proto3" json:"weight,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -148,7 +150,7 @@ func (m *Metadata) Reset() { *m = Metadata{} } func (m *Metadata) String() string { return proto.CompactTextString(m) } func (*Metadata) ProtoMessage() {} func (*Metadata) Descriptor() ([]byte, []int) { - return fileDescriptor_metadata_d6c714c73a051dcb, []int{1} + return fileDescriptor_metadata_a633fda7c87700ff, []int{1} } func (m *Metadata) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Metadata.Unmarshal(m, b) @@ -287,6 +289,13 @@ func (m *Metadata) GetKubeVersion() string { return "" } +func (m *Metadata) GetWeight() uint32 { + if m != nil { + return m.Weight + } + return 0 +} + func init() { proto.RegisterType((*Maintainer)(nil), "hapi.chart.Maintainer") proto.RegisterType((*Metadata)(nil), "hapi.chart.Metadata") @@ -294,36 +303,37 @@ func init() { proto.RegisterEnum("hapi.chart.Metadata_Engine", Metadata_Engine_name, Metadata_Engine_value) } -func init() { proto.RegisterFile("hapi/chart/metadata.proto", fileDescriptor_metadata_d6c714c73a051dcb) } +func init() { proto.RegisterFile("hapi/chart/metadata.proto", fileDescriptor_metadata_a633fda7c87700ff) } -var fileDescriptor_metadata_d6c714c73a051dcb = []byte{ - // 435 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x52, 0x5d, 0x6b, 0xd4, 0x40, - 0x14, 0x35, 0xcd, 0x66, 0x77, 0x73, 0x63, 0x35, 0x0e, 0x52, 0xc6, 0x22, 0x12, 0x16, 0x85, 0x7d, +var fileDescriptor_metadata_a633fda7c87700ff = []byte{ + // 449 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x53, 0x5d, 0x6b, 0xd4, 0x40, + 0x14, 0x35, 0x4d, 0xb3, 0xbb, 0xb9, 0x71, 0x35, 0x5e, 0xa4, 0x8c, 0x45, 0x24, 0x2c, 0x0a, 0x79, 0xda, 0x82, 0xbe, 0x14, 0x1f, 0x04, 0x85, 0x52, 0x41, 0xbb, 0x95, 0xe0, 0x07, 0xf8, 0x36, 0x4d, - 0x2e, 0xdd, 0x61, 0x93, 0x99, 0x30, 0x99, 0xad, 0xec, 0xaf, 0xf0, 0x2f, 0xcb, 0xdc, 0x64, 0x9a, - 0xac, 0xf4, 0xed, 0x9e, 0x73, 0x66, 0xce, 0xcc, 0xbd, 0xf7, 0xc0, 0x8b, 0x8d, 0x68, 0xe4, 0x59, - 0xb1, 0x11, 0xc6, 0x9e, 0xd5, 0x68, 0x45, 0x29, 0xac, 0x58, 0x35, 0x46, 0x5b, 0xcd, 0xc0, 0x49, - 0x2b, 0x92, 0x16, 0x9f, 0x01, 0xae, 0x84, 0x54, 0x56, 0x48, 0x85, 0x86, 0x31, 0x98, 0x28, 0x51, - 0x23, 0x0f, 0xb2, 0x60, 0x19, 0xe7, 0x54, 0xb3, 0xe7, 0x10, 0x61, 0x2d, 0x64, 0xc5, 0x8f, 0x88, - 0xec, 0x00, 0x4b, 0x21, 0xdc, 0x99, 0x8a, 0x87, 0xc4, 0xb9, 0x72, 0xf1, 0x37, 0x82, 0xf9, 0x55, - 0xff, 0xd0, 0x83, 0x46, 0x0c, 0x26, 0x1b, 0x5d, 0x63, 0xef, 0x43, 0x35, 0xe3, 0x30, 0x6b, 0xf5, - 0xce, 0x14, 0xd8, 0xf2, 0x30, 0x0b, 0x97, 0x71, 0xee, 0xa1, 0x53, 0xee, 0xd0, 0xb4, 0x52, 0x2b, - 0x3e, 0xa1, 0x0b, 0x1e, 0xb2, 0x0c, 0x92, 0x12, 0xdb, 0xc2, 0xc8, 0xc6, 0x3a, 0x35, 0x22, 0x75, - 0x4c, 0xb1, 0x53, 0x98, 0x6f, 0x71, 0xff, 0x47, 0x9b, 0xb2, 0xe5, 0x53, 0xb2, 0xbd, 0xc7, 0xec, - 0x1c, 0x92, 0xfa, 0xbe, 0xe1, 0x96, 0xcf, 0xb2, 0x70, 0x99, 0xbc, 0x3d, 0x59, 0x0d, 0x23, 0x59, - 0x0d, 0xf3, 0xc8, 0xc7, 0x47, 0xd9, 0x09, 0x4c, 0x51, 0xdd, 0x4a, 0x85, 0x7c, 0x4e, 0x4f, 0xf6, - 0xc8, 0xf5, 0x25, 0x0b, 0xad, 0x78, 0xdc, 0xf5, 0xe5, 0x6a, 0xf6, 0x0a, 0x40, 0x34, 0xf2, 0x67, - 0xdf, 0x00, 0x90, 0x32, 0x62, 0xd8, 0x4b, 0x88, 0x0b, 0xad, 0x4a, 0x49, 0x1d, 0x24, 0x24, 0x0f, - 0x84, 0x73, 0xb4, 0xe2, 0xb6, 0xe5, 0x8f, 0x3b, 0x47, 0x57, 0x77, 0x8e, 0x8d, 0x77, 0x3c, 0xf6, - 0x8e, 0x9e, 0x71, 0x7a, 0x89, 0x8d, 0xc1, 0x42, 0x58, 0x2c, 0xf9, 0x93, 0x2c, 0x58, 0xce, 0xf3, - 0x11, 0xc3, 0x5e, 0xc3, 0xb1, 0x95, 0x55, 0x85, 0xc6, 0x5b, 0x3c, 0x25, 0x8b, 0x43, 0x92, 0x5d, - 0x42, 0x22, 0x94, 0xd2, 0x56, 0xb8, 0x7f, 0xb4, 0x3c, 0xa5, 0xe9, 0xbc, 0x39, 0x98, 0x8e, 0xcf, - 0xd2, 0xc7, 0xe1, 0xdc, 0x85, 0xb2, 0x66, 0x9f, 0x8f, 0x6f, 0xba, 0x25, 0x6d, 0x77, 0x37, 0xe8, - 0x1f, 0x7b, 0xd6, 0x2d, 0x69, 0x44, 0x9d, 0x7e, 0x80, 0xf4, 0x7f, 0x0b, 0x97, 0xaa, 0x2d, 0xee, - 0xfb, 0xd4, 0xb8, 0xd2, 0xa5, 0xef, 0x4e, 0x54, 0x3b, 0x9f, 0x9a, 0x0e, 0xbc, 0x3f, 0x3a, 0x0f, - 0x16, 0x19, 0x4c, 0x2f, 0xba, 0x05, 0x24, 0x30, 0xfb, 0xb1, 0xfe, 0xb2, 0xbe, 0xfe, 0xb5, 0x4e, - 0x1f, 0xb1, 0x18, 0xa2, 0xcb, 0xeb, 0xef, 0xdf, 0xbe, 0xa6, 0xc1, 0xa7, 0xd9, 0xef, 0x88, 0xfe, - 0x7c, 0x33, 0xa5, 0xdc, 0xbf, 0xfb, 0x17, 0x00, 0x00, 0xff, 0xff, 0x36, 0xf9, 0x0d, 0xa6, 0x14, - 0x03, 0x00, 0x00, + 0x2e, 0xbb, 0xc3, 0x26, 0x33, 0x61, 0x32, 0xdb, 0xb2, 0xbf, 0xc8, 0xbf, 0x29, 0x33, 0xc9, 0x74, + 0x53, 0xf1, 0xed, 0x9e, 0x73, 0x66, 0xce, 0xe4, 0xdc, 0x7b, 0x03, 0x2f, 0x36, 0xbc, 0x15, 0x67, + 0xe5, 0x86, 0x6b, 0x73, 0xd6, 0x90, 0xe1, 0x15, 0x37, 0x7c, 0xd9, 0x6a, 0x65, 0x14, 0x82, 0x95, + 0x96, 0x4e, 0x5a, 0x7c, 0x06, 0xb8, 0xe2, 0x42, 0x1a, 0x2e, 0x24, 0x69, 0x44, 0x38, 0x96, 0xbc, + 0x21, 0x16, 0x64, 0x41, 0x1e, 0x17, 0xae, 0xc6, 0xe7, 0x10, 0x51, 0xc3, 0x45, 0xcd, 0x8e, 0x1c, + 0xd9, 0x03, 0x4c, 0x21, 0xdc, 0xe9, 0x9a, 0x85, 0x8e, 0xb3, 0xe5, 0xe2, 0x4f, 0x04, 0xb3, 0xab, + 0xe1, 0xa1, 0xff, 0x1a, 0x21, 0x1c, 0x6f, 0x54, 0x43, 0x83, 0x8f, 0xab, 0x91, 0xc1, 0xb4, 0x53, + 0x3b, 0x5d, 0x52, 0xc7, 0xc2, 0x2c, 0xcc, 0xe3, 0xc2, 0x43, 0xab, 0xdc, 0x92, 0xee, 0x84, 0x92, + 0xec, 0xd8, 0x5d, 0xf0, 0x10, 0x33, 0x48, 0x2a, 0xea, 0x4a, 0x2d, 0x5a, 0x63, 0xd5, 0xc8, 0xa9, + 0x63, 0x0a, 0x4f, 0x61, 0xb6, 0xa5, 0xfd, 0x9d, 0xd2, 0x55, 0xc7, 0x26, 0xce, 0xf6, 0x1e, 0xe3, + 0x39, 0x24, 0xcd, 0x7d, 0xe0, 0x8e, 0x4d, 0xb3, 0x30, 0x4f, 0xde, 0x9e, 0x2c, 0x0f, 0x2d, 0x59, + 0x1e, 0xfa, 0x51, 0x8c, 0x8f, 0xe2, 0x09, 0x4c, 0x48, 0xae, 0x85, 0x24, 0x36, 0x73, 0x4f, 0x0e, + 0xc8, 0xe6, 0x12, 0xa5, 0x92, 0x2c, 0xee, 0x73, 0xd9, 0x1a, 0x5f, 0x01, 0xf0, 0x56, 0xfc, 0x1c, + 0x02, 0x80, 0x53, 0x46, 0x0c, 0xbe, 0x84, 0xb8, 0x54, 0xb2, 0x12, 0x2e, 0x41, 0xe2, 0xe4, 0x03, + 0x61, 0x1d, 0x0d, 0x5f, 0x77, 0xec, 0x71, 0xef, 0x68, 0xeb, 0xde, 0xb1, 0xf5, 0x8e, 0x73, 0xef, + 0xe8, 0x19, 0xab, 0x57, 0xd4, 0x6a, 0x2a, 0xb9, 0xa1, 0x8a, 0x3d, 0xc9, 0x82, 0x7c, 0x56, 0x8c, + 0x18, 0x7c, 0x0d, 0x73, 0x23, 0xea, 0x9a, 0xb4, 0xb7, 0x78, 0xea, 0x2c, 0x1e, 0x92, 0x78, 0x09, + 0x09, 0x97, 0x52, 0x19, 0x6e, 0xbf, 0xa3, 0x63, 0xa9, 0xeb, 0xce, 0x9b, 0x07, 0xdd, 0xf1, 0xbb, + 0xf4, 0xf1, 0x70, 0xee, 0x42, 0x1a, 0xbd, 0x2f, 0xc6, 0x37, 0xed, 0x90, 0xb6, 0xbb, 0x1b, 0xf2, + 0x8f, 0x3d, 0xeb, 0x87, 0x34, 0xa2, 0x6c, 0x3b, 0xef, 0x48, 0xac, 0x37, 0x86, 0x61, 0x16, 0xe4, + 0xf3, 0x62, 0x40, 0xa7, 0x1f, 0x20, 0xfd, 0xd7, 0xda, 0x6e, 0xdb, 0x96, 0xf6, 0xc3, 0x36, 0xd9, + 0xd2, 0x6e, 0xe5, 0x2d, 0xaf, 0x77, 0x7e, 0x9b, 0x7a, 0xf0, 0xfe, 0xe8, 0x3c, 0x58, 0x64, 0x30, + 0xb9, 0xe8, 0x07, 0x93, 0xc0, 0xf4, 0xc7, 0xea, 0xcb, 0xea, 0xfa, 0xd7, 0x2a, 0x7d, 0x84, 0x31, + 0x44, 0x97, 0xd7, 0xdf, 0xbf, 0x7d, 0x4d, 0x83, 0x4f, 0xd3, 0xdf, 0x91, 0xcb, 0x72, 0x33, 0x71, + 0xff, 0xc3, 0xbb, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x15, 0xc1, 0x9c, 0xdf, 0x2c, 0x03, 0x00, + 0x00, } diff --git a/pkg/releasetesting/test_suite.go b/pkg/releasetesting/test_suite.go index 4fa5de526..e6ffda48f 100644 --- a/pkg/releasetesting/test_suite.go +++ b/pkg/releasetesting/test_suite.go @@ -27,6 +27,7 @@ import ( "k8s.io/api/core/v1" "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/manifest" "k8s.io/helm/pkg/proto/hapi/release" util "k8s.io/helm/pkg/releaseutil" "k8s.io/helm/pkg/timeconv" @@ -206,7 +207,7 @@ func extractTestManifestsFromHooks(h []*release.Hook) ([]string, error) { } func newTest(testManifest string) (*test, error) { - var sh util.SimpleHead + var sh manifest.SimpleHead err := yaml.Unmarshal([]byte(testManifest), &sh) if err != nil { return nil, err diff --git a/pkg/releaseutil/manifest.go b/pkg/releaseutil/manifest.go index 78c2979c4..262c41d02 100644 --- a/pkg/releaseutil/manifest.go +++ b/pkg/releaseutil/manifest.go @@ -19,20 +19,17 @@ package releaseutil import ( "fmt" "regexp" + "strconv" "strings" -) -// SimpleHead defines what the structure of the head of a manifest file -type SimpleHead struct { - Version string `json:"apiVersion"` - Kind string `json:"kind,omitempty"` - Metadata *struct { - Name string `json:"name"` - Annotations map[string]string `json:"annotations"` - } `json:"metadata,omitempty"` -} + "k8s.io/helm/pkg/manifest" +) -var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*") +var ( + sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*") + wei = regexp.MustCompile("\\#\\s*Weight\\:\\s*(\\d+),(\\d+)\\s*") + path = regexp.MustCompile("\\#\\s*Source\\:\\s*(.*)\\s*") +) // SplitManifests takes a string of manifest and returns a map contains individual manifests func SplitManifests(bigFile string) map[string]string { @@ -45,6 +42,7 @@ func SplitManifests(bigFile string) map[string]string { bigFileTmp := strings.TrimSpace(bigFile) docs := sep.Split(bigFileTmp, -1) var count int + var name string for _, d := range docs { if d == "" { @@ -52,8 +50,60 @@ func SplitManifests(bigFile string) map[string]string { } d = strings.TrimSpace(d) - res[fmt.Sprintf(tpl, count)] = d + match := path.FindStringSubmatch(d) + if match != nil { + name = match[1] + } else { + name = fmt.Sprintf(tpl, count) + } + res[name] = d count = count + 1 } return res } + +// SplitManifestContent takes a string of manifest and returns a slice containing individual manifests +func SplitManifestContent(bigFile string) []string { + res := []string{} + bigFileTmp := strings.TrimSpace(bigFile) + docs := sep.Split(bigFileTmp, -1) + for _, d := range docs { + if d == "" { + continue + } + res = append(res, d) + } + return res +} + +// GroupManifestsByWeight takes a map of manifests and returns an ordered grouping of them +func GroupManifestsByWeight(manifests []string) []string { + groups := []string{} + var groupWeight *manifest.Weight + var weight *manifest.Weight + + for _, m := range manifests { + weight = getManifestWeight(m) + tmp := fmt.Sprintf("---\n%s\n", m) + if weight.Equalto(groupWeight) { + groups[len(groups)-1] += tmp + } else { + groups = append(groups, tmp) + groupWeight = weight + } + } + return groups +} + +func getManifestWeight(file string) (weight *manifest.Weight) { + weight = new(manifest.Weight) + match := wei.FindStringSubmatch(file) + if match == nil { + return + } + c, _ := strconv.ParseUint(match[1], 10, 32) + m, _ := strconv.ParseUint(match[2], 10, 32) + weight.Chart = uint32(c) + weight.Manifest = uint32(m) + return +} diff --git a/pkg/tiller/environment/environment.go b/pkg/tiller/environment/environment.go index d3a478ea0..ff498dff8 100644 --- a/pkg/tiller/environment/environment.go +++ b/pkg/tiller/environment/environment.go @@ -156,6 +156,8 @@ type KubeClient interface { // by "\n---\n"). UpdateWithOptions(namespace string, originalReader, modifiedReader io.Reader, opts kube.UpdateOptions) error + RemoveDiff(namespace string, originalReader, targetReader io.Reader) error + Build(namespace string, reader io.Reader) (kube.Result, error) // BuildUnstructured reads a stream of manifests from a reader and turns them into @@ -234,6 +236,12 @@ func (p *PrintingKubeClient) UpdateWithOptions(ns string, currentReader, modifie return err } +// RemoveDiff implements KubeClient RemoveDiff. +func (p *PrintingKubeClient) RemoveDiff(namespace string, originalReader, targetReader io.Reader) error { + _, err := io.Copy(p.Out, targetReader) + return err +} + // Build implements KubeClient Build. func (p *PrintingKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { return []*resource.Info{}, nil diff --git a/pkg/tiller/environment/environment_test.go b/pkg/tiller/environment/environment_test.go index 962ff4d93..17112b9c9 100644 --- a/pkg/tiller/environment/environment_test.go +++ b/pkg/tiller/environment/environment_test.go @@ -61,6 +61,9 @@ func (k *mockKubeClient) UpdateWithOptions(ns string, currentReader, modifiedRea func (k *mockKubeClient) WatchUntilReady(ns string, r io.Reader, timeout int64, shouldWait bool) error { return nil } +func (k *mockKubeClient) RemoveDiff(namespace string, originalReader, targetReader io.Reader) error { + return nil +} func (k *mockKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { return []*resource.Info{}, nil } diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go index 0eae3c475..463d1656c 100644 --- a/pkg/tiller/hooks.go +++ b/pkg/tiller/hooks.go @@ -19,6 +19,7 @@ package tiller import ( "fmt" "log" + "os" "path" "strconv" "strings" @@ -28,10 +29,21 @@ import ( "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/hooks" "k8s.io/helm/pkg/manifest" + "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" util "k8s.io/helm/pkg/releaseutil" ) +// SortType is used for determining sort function +type SortType int + +const ( + // SortInstall is used for installing a chart + SortInstall SortType = iota + // SortUninstall is used for removing a chart + SortUninstall +) + var events = map[string]release.Hook_Event{ hooks.PreInstall: release.Hook_PRE_INSTALL, hooks.PostInstall: release.Hook_POST_INSTALL, @@ -79,7 +91,7 @@ type manifestFile struct { // // Files that do not parse into the expected format are simply placed into a map and // returned. -func sortManifests(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []Manifest, error) { +func sortManifests(ch *chart.Chart, files map[string]string, apis chartutil.VersionSet, sort SortType) ([]*release.Hook, []Manifest, error) { result := &result{} for filePath, c := range files { @@ -101,12 +113,12 @@ func sortManifests(files map[string]string, apis chartutil.VersionSet, sort Sort apis: apis, } - if err := manifestFile.sort(result); err != nil { + if err := manifestFile.sort(ch, result); err != nil { return result.hooks, result.generic, err } } - return result.hooks, sortByKind(result.generic, sort), nil + return result.hooks, sortByWeight(sortByKind(result.generic, sort), sort), nil } // sort takes a manifestFile object which may contain multiple resource definition @@ -128,9 +140,9 @@ func sortManifests(files map[string]string, apis chartutil.VersionSet, sort Sort // metadata: // annotations: // helm.sh/hook-delete-policy: hook-succeeded -func (file *manifestFile) sort(result *result) error { +func (file *manifestFile) sort(ch *chart.Chart, result *result) error { for _, m := range file.entries { - var entry util.SimpleHead + var entry manifest.SimpleHead err := yaml.Unmarshal([]byte(m), &entry) if err != nil { @@ -138,21 +150,35 @@ func (file *manifestFile) sort(result *result) error { return e } + var weight manifest.Weight + if ch != nil { + weight = manifest.Weight{ + Chart: getChartWeight(ch, file.path), + Manifest: 0, + } + } + if !hasAnyAnnotation(entry) { result.generic = append(result.generic, Manifest{ Name: file.path, Content: m, Head: &entry, + Weight: &weight, }) continue } + if mw, err := strconv.ParseUint(entry.Metadata.Annotations[manifest.ManifestOrderWeight], 10, 32); err == nil { + weight.Manifest = uint32(mw) + } + hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno] if !ok { result.generic = append(result.generic, Manifest{ Name: file.path, Content: m, Head: &entry, + Weight: &weight, }) continue } @@ -211,7 +237,35 @@ func (file *manifestFile) sort(result *result) error { return nil } -func hasAnyAnnotation(entry util.SimpleHead) bool { +func getChartWeight(ch *chart.Chart, name string) uint32 { + if ch == nil { + return 0 + } + + for _, tpl := range ch.Templates { + if path.Base(tpl.Name) == path.Base(name) && ch.Metadata.Name == getOwnerChart(name) { + return ch.Metadata.Weight + } + } + + for _, chart := range ch.Dependencies { + if w := getChartWeight(chart, name); w != 0 { + return w + } + } + + return 0 +} + +func getOwnerChart(path string) string { + parts := strings.Split(path, string(os.PathSeparator)) + if len(parts) >= 3 { + return parts[len(parts)-3] + } + return "" +} + +func hasAnyAnnotation(entry manifest.SimpleHead) bool { if entry.Metadata == nil || entry.Metadata.Annotations == nil || len(entry.Metadata.Annotations) == 0 { @@ -221,7 +275,7 @@ func hasAnyAnnotation(entry util.SimpleHead) bool { return true } -func calculateHookWeight(entry util.SimpleHead) int32 { +func calculateHookWeight(entry manifest.SimpleHead) int32 { hws := entry.Metadata.Annotations[hooks.HookWeightAnno] hw, err := strconv.Atoi(hws) if err != nil { @@ -231,7 +285,7 @@ func calculateHookWeight(entry util.SimpleHead) int32 { return int32(hw) } -func operateAnnotationValues(entry util.SimpleHead, annotation string, operate func(p string)) { +func operateAnnotationValues(entry manifest.SimpleHead, annotation string, operate func(p string)) { if dps, ok := entry.Metadata.Annotations[annotation]; ok { for _, dp := range strings.Split(dps, ",") { dp = strings.ToLower(strings.TrimSpace(dp)) diff --git a/pkg/tiller/hooks_test.go b/pkg/tiller/hooks_test.go index daf07252e..b072f7762 100644 --- a/pkg/tiller/hooks_test.go +++ b/pkg/tiller/hooks_test.go @@ -25,6 +25,7 @@ import ( "github.com/ghodss/yaml" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/manifest" "k8s.io/helm/pkg/proto/hapi/release" util "k8s.io/helm/pkg/releaseutil" ) @@ -142,7 +143,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := sortManifests(manifests, chartutil.NewVersionSet("v1", "v1beta1"), InstallOrder) + hs, generic, err := sortManifests(nil, manifests, chartutil.NewVersionSet("v1", "v1beta1"), SortInstall) if err != nil { t.Fatalf("Unexpected error: %s", err) } @@ -201,7 +202,7 @@ metadata: manifests := util.SplitManifests(s.manifest) for _, m := range manifests { - var sh util.SimpleHead + var sh manifest.SimpleHead err := yaml.Unmarshal([]byte(m), &sh) if err != nil { // This is expected for manifests that are corrupt or empty. @@ -223,7 +224,7 @@ metadata: } } - sorted = sortByKind(sorted, InstallOrder) + sorted = sortByKind(sorted, SortInstall) for i, m := range generic { if m.Content != sorted[i].Content { t.Errorf("Expected %q, got %q", m.Content, sorted[i].Content) @@ -304,7 +305,7 @@ func TestSortManifestsHookDeletion(t *testing.T) { "exampleManifest": buf.String(), } - hs, _, err := sortManifests(manifests, chartutil.NewVersionSet("v1", "v1beta1"), InstallOrder) + hs, _, err := sortManifests(nil, manifests, chartutil.NewVersionSet("v1", "v1beta1"), SortInstall) if err != nil { t.Error(err) } diff --git a/pkg/tiller/kind_sorter.go b/pkg/tiller/kind_sorter.go index 5c7193dee..228f97fb3 100644 --- a/pkg/tiller/kind_sorter.go +++ b/pkg/tiller/kind_sorter.go @@ -102,7 +102,13 @@ var UninstallOrder SortOrder = []string{ // sortByKind does an in-place sort of manifests by Kind. // // Results are sorted by 'ordering' -func sortByKind(manifests []Manifest, ordering SortOrder) []Manifest { +func sortByKind(manifests []Manifest, st SortType) []Manifest { + var ordering SortOrder + if st == SortInstall { + ordering = InstallOrder + } else { + ordering = UninstallOrder + } ks := newKindSorter(manifests, ordering) sort.Sort(ks) return ks.manifests diff --git a/pkg/tiller/kind_sorter_test.go b/pkg/tiller/kind_sorter_test.go index 3b37256ed..2bf216284 100644 --- a/pkg/tiller/kind_sorter_test.go +++ b/pkg/tiller/kind_sorter_test.go @@ -20,152 +20,152 @@ import ( "bytes" "testing" - util "k8s.io/helm/pkg/releaseutil" + "k8s.io/helm/pkg/manifest" ) func TestKindSorter(t *testing.T) { manifests := []Manifest{ { Name: "i", - Head: &util.SimpleHead{Kind: "ClusterRole"}, + Head: &manifest.SimpleHead{Kind: "ClusterRole"}, }, { Name: "I", - Head: &util.SimpleHead{Kind: "ClusterRoleList"}, + Head: &manifest.SimpleHead{Kind: "ClusterRoleList"}, }, { Name: "j", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, + Head: &manifest.SimpleHead{Kind: "ClusterRoleBinding"}, }, { Name: "J", - Head: &util.SimpleHead{Kind: "ClusterRoleBindingList"}, + Head: &manifest.SimpleHead{Kind: "ClusterRoleBindingList"}, }, { Name: "e", - Head: &util.SimpleHead{Kind: "ConfigMap"}, + Head: &manifest.SimpleHead{Kind: "ConfigMap"}, }, { Name: "u", - Head: &util.SimpleHead{Kind: "CronJob"}, + Head: &manifest.SimpleHead{Kind: "CronJob"}, }, { Name: "2", - Head: &util.SimpleHead{Kind: "CustomResourceDefinition"}, + Head: &manifest.SimpleHead{Kind: "CustomResourceDefinition"}, }, { Name: "n", - Head: &util.SimpleHead{Kind: "DaemonSet"}, + Head: &manifest.SimpleHead{Kind: "DaemonSet"}, }, { Name: "r", - Head: &util.SimpleHead{Kind: "Deployment"}, + Head: &manifest.SimpleHead{Kind: "Deployment"}, }, { Name: "!", - Head: &util.SimpleHead{Kind: "HonkyTonkSet"}, + Head: &manifest.SimpleHead{Kind: "HonkyTonkSet"}, }, { Name: "v", - Head: &util.SimpleHead{Kind: "Ingress"}, + Head: &manifest.SimpleHead{Kind: "Ingress"}, }, { Name: "t", - Head: &util.SimpleHead{Kind: "Job"}, + Head: &manifest.SimpleHead{Kind: "Job"}, }, { Name: "c", - Head: &util.SimpleHead{Kind: "LimitRange"}, + Head: &manifest.SimpleHead{Kind: "LimitRange"}, }, { Name: "a", - Head: &util.SimpleHead{Kind: "Namespace"}, + Head: &manifest.SimpleHead{Kind: "Namespace"}, }, { Name: "f", - Head: &util.SimpleHead{Kind: "PersistentVolume"}, + Head: &manifest.SimpleHead{Kind: "PersistentVolume"}, }, { Name: "g", - Head: &util.SimpleHead{Kind: "PersistentVolumeClaim"}, + Head: &manifest.SimpleHead{Kind: "PersistentVolumeClaim"}, }, { Name: "o", - Head: &util.SimpleHead{Kind: "Pod"}, + Head: &manifest.SimpleHead{Kind: "Pod"}, }, { Name: "3", - Head: &util.SimpleHead{Kind: "PodSecurityPolicy"}, + Head: &manifest.SimpleHead{Kind: "PodSecurityPolicy"}, }, { Name: "q", - Head: &util.SimpleHead{Kind: "ReplicaSet"}, + Head: &manifest.SimpleHead{Kind: "ReplicaSet"}, }, { Name: "p", - Head: &util.SimpleHead{Kind: "ReplicationController"}, + Head: &manifest.SimpleHead{Kind: "ReplicationController"}, }, { Name: "b", - Head: &util.SimpleHead{Kind: "ResourceQuota"}, + Head: &manifest.SimpleHead{Kind: "ResourceQuota"}, }, { Name: "k", - Head: &util.SimpleHead{Kind: "Role"}, + Head: &manifest.SimpleHead{Kind: "Role"}, }, { Name: "K", - Head: &util.SimpleHead{Kind: "RoleList"}, + Head: &manifest.SimpleHead{Kind: "RoleList"}, }, { Name: "l", - Head: &util.SimpleHead{Kind: "RoleBinding"}, + Head: &manifest.SimpleHead{Kind: "RoleBinding"}, }, { Name: "L", - Head: &util.SimpleHead{Kind: "RoleBindingList"}, + Head: &manifest.SimpleHead{Kind: "RoleBindingList"}, }, { Name: "d", - Head: &util.SimpleHead{Kind: "Secret"}, + Head: &manifest.SimpleHead{Kind: "Secret"}, }, { Name: "m", - Head: &util.SimpleHead{Kind: "Service"}, + Head: &manifest.SimpleHead{Kind: "Service"}, }, { Name: "h", - Head: &util.SimpleHead{Kind: "ServiceAccount"}, + Head: &manifest.SimpleHead{Kind: "ServiceAccount"}, }, { Name: "s", - Head: &util.SimpleHead{Kind: "StatefulSet"}, + Head: &manifest.SimpleHead{Kind: "StatefulSet"}, }, { Name: "1", - Head: &util.SimpleHead{Kind: "StorageClass"}, + Head: &manifest.SimpleHead{Kind: "StorageClass"}, }, { Name: "w", - Head: &util.SimpleHead{Kind: "APIService"}, + Head: &manifest.SimpleHead{Kind: "APIService"}, }, { Name: "z", - Head: &util.SimpleHead{Kind: "PodDisruptionBudget"}, + Head: &manifest.SimpleHead{Kind: "PodDisruptionBudget"}, }, { Name: "x", - Head: &util.SimpleHead{Kind: "HorizontalPodAutoscaler"}, + Head: &manifest.SimpleHead{Kind: "HorizontalPodAutoscaler"}, }, } for _, test := range []struct { description string - order SortOrder + order SortType expected string }{ - {"install", InstallOrder, "abc3zde1fgh2iIjJkKlLmnopqrxstuvw!"}, - {"uninstall", UninstallOrder, "wvmutsxrqponLlKkJjIi2hgf1edz3cba!"}, + {"install", SortInstall, "abc3zde1fgh2iIjJkKlLmnopqrxstuvw!"}, + {"uninstall", SortUninstall, "wvmutsxrqponLlKkJjIi2hgf1edz3cba!"}, } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { @@ -188,48 +188,48 @@ func TestKindSorterSubSort(t *testing.T) { manifests := []Manifest{ { Name: "a", - Head: &util.SimpleHead{Kind: "ClusterRole"}, + Head: &manifest.SimpleHead{Kind: "ClusterRole"}, }, { Name: "A", - Head: &util.SimpleHead{Kind: "ClusterRole"}, + Head: &manifest.SimpleHead{Kind: "ClusterRole"}, }, { Name: "0", - Head: &util.SimpleHead{Kind: "ConfigMap"}, + Head: &manifest.SimpleHead{Kind: "ConfigMap"}, }, { Name: "1", - Head: &util.SimpleHead{Kind: "ConfigMap"}, + Head: &manifest.SimpleHead{Kind: "ConfigMap"}, }, { Name: "z", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, + Head: &manifest.SimpleHead{Kind: "ClusterRoleBinding"}, }, { Name: "!", - Head: &util.SimpleHead{Kind: "ClusterRoleBinding"}, + Head: &manifest.SimpleHead{Kind: "ClusterRoleBinding"}, }, { Name: "u2", - Head: &util.SimpleHead{Kind: "Unknown"}, + Head: &manifest.SimpleHead{Kind: "Unknown"}, }, { Name: "u1", - Head: &util.SimpleHead{Kind: "Unknown"}, + Head: &manifest.SimpleHead{Kind: "Unknown"}, }, { Name: "t3", - Head: &util.SimpleHead{Kind: "Unknown2"}, + Head: &manifest.SimpleHead{Kind: "Unknown2"}, }, } for _, test := range []struct { description string - order SortOrder + order SortType expected string }{ // expectation is sorted by kind (unknown is last) and then sub sorted alphabetically within each group - {"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01Aa!zu1u2t3"}, + {"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", SortInstall, "01Aa!zu1u2t3"}, } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { @@ -247,15 +247,15 @@ func TestKindSorterSubSort(t *testing.T) { func TestKindSorterNamespaceAgainstUnknown(t *testing.T) { unknown := Manifest{ Name: "a", - Head: &util.SimpleHead{Kind: "Unknown"}, + Head: &manifest.SimpleHead{Kind: "Unknown"}, } namespace := Manifest{ Name: "b", - Head: &util.SimpleHead{Kind: "Namespace"}, + Head: &manifest.SimpleHead{Kind: "Namespace"}, } manifests := []Manifest{unknown, namespace} - sortByKind(manifests, InstallOrder) + sortByKind(manifests, SortInstall) expectedOrder := []Manifest{namespace, unknown} for i, manifest := range manifests { diff --git a/pkg/tiller/release_install_test.go b/pkg/tiller/release_install_test.go index 78d00ce66..6f138dfd9 100644 --- a/pkg/tiller/release_install_test.go +++ b/pkg/tiller/release_install_test.go @@ -105,7 +105,7 @@ func TestInstallRelease(t *testing.T) { t.Errorf("Expected manifest in %v", res) } - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { + if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\n# Weight: 0,0\nhello: world") { t.Errorf("unexpected output: %s", rel.Manifest) } @@ -165,7 +165,7 @@ func TestInstallRelease_WithNotes(t *testing.T) { t.Errorf("Expected manifest in %v", res) } - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { + if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\n# Weight: 0,0\nhello: world") { t.Errorf("unexpected output: %s", rel.Manifest) } @@ -226,7 +226,7 @@ func TestInstallRelease_WithNotesRendered(t *testing.T) { t.Errorf("Expected manifest in %v", res) } - if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { + if !strings.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\n# Weight: 0,0\nhello: world") { t.Errorf("unexpected output: %s", rel.Manifest) } @@ -348,11 +348,11 @@ func TestInstallRelease_DryRun(t *testing.T) { t.Errorf("Expected release name.") } - if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { + if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/templates/hello\n# Weight: 0,0\nhello: world") { t.Errorf("unexpected output: %s", res.Release.Manifest) } - if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world") { + if !strings.Contains(res.Release.Manifest, "---\n# Source: hello/templates/goodbye\n# Weight: 0,0\ngoodbye: world") { t.Errorf("unexpected output: %s", res.Release.Manifest) } diff --git a/pkg/tiller/release_modules.go b/pkg/tiller/release_modules.go index 360794481..abe85896f 100644 --- a/pkg/tiller/release_modules.go +++ b/pkg/tiller/release_modules.go @@ -50,34 +50,108 @@ type LocalReleaseModule struct { // Create creates a release via kubeclient from provided environment func (m *LocalReleaseModule) Create(r *release.Release, req *services.InstallReleaseRequest, env *environment.Environment) error { - b := bytes.NewBufferString(r.Manifest) - return env.KubeClient.Create(r.Namespace, b, req.Timeout, req.Wait) + if !req.Wait { + b := bytes.NewBufferString(r.Manifest) + return env.KubeClient.Create(r.Namespace, b, req.Timeout, req.Wait) + } + + manifests := relutil.GroupManifestsByWeight(relutil.SplitManifestContent(r.Manifest)) + for _, manifest := range manifests { + b := bytes.NewBufferString(manifest) + if err := env.KubeClient.Create(r.Namespace, b, req.Timeout, req.Wait); err != nil { + return err + } + } + + return nil } // Update performs an update from current to target release func (m *LocalReleaseModule) Update(current, target *release.Release, req *services.UpdateReleaseRequest, env *environment.Environment) error { - c := bytes.NewBufferString(current.Manifest) - t := bytes.NewBufferString(target.Manifest) - return env.KubeClient.UpdateWithOptions(target.Namespace, c, t, kube.UpdateOptions{ - Force: req.Force, - Recreate: req.Recreate, - Timeout: req.Timeout, - ShouldWait: req.Wait, - CleanupOnFail: req.CleanupOnFail, - }) + var err error + var t *bytes.Buffer + var c *bytes.Buffer + + if !req.Wait { + c := bytes.NewBufferString(current.Manifest) + t := bytes.NewBufferString(target.Manifest) + if err = env.KubeClient.UpdateWithOptions(target.Namespace, c, t, kube.UpdateOptions{ + Force: req.Force, + Recreate: req.Recreate, + Timeout: req.Timeout, + ShouldWait: req.Wait, + CleanupOnFail: req.CleanupOnFail, + }); err != nil { + return err + } + } else { + manifests := relutil.GroupManifestsByWeight(relutil.SplitManifestContent(target.Manifest)) + for _, manifest := range manifests { + c = bytes.NewBufferString(current.Manifest) + t = bytes.NewBufferString(manifest) + if err = env.KubeClient.UpdateWithOptions(target.Namespace, c, t, kube.UpdateOptions{ + Force: req.Force, + Recreate: req.Recreate, + Timeout: req.Timeout, + ShouldWait: req.Wait, + CleanupOnFail: req.CleanupOnFail, + }); err != nil { + return err + } + } + } + + c = bytes.NewBufferString(current.Manifest) + t = bytes.NewBufferString(target.Manifest) + if err = env.KubeClient.RemoveDiff(target.Namespace, c, t); err != nil { + return err + } + + return nil } // Rollback performs a rollback from current to target release func (m *LocalReleaseModule) Rollback(current, target *release.Release, req *services.RollbackReleaseRequest, env *environment.Environment) error { - c := bytes.NewBufferString(current.Manifest) - t := bytes.NewBufferString(target.Manifest) - return env.KubeClient.UpdateWithOptions(target.Namespace, c, t, kube.UpdateOptions{ - Force: req.Force, - Recreate: req.Recreate, - Timeout: req.Timeout, - ShouldWait: req.Wait, - CleanupOnFail: req.CleanupOnFail, - }) + var err error + var t *bytes.Buffer + var c *bytes.Buffer + + if !req.Wait { + c := bytes.NewBufferString(current.Manifest) + t := bytes.NewBufferString(target.Manifest) + if err = env.KubeClient.UpdateWithOptions(target.Namespace, c, t, kube.UpdateOptions{ + Force: req.Force, + Recreate: req.Recreate, + Timeout: req.Timeout, + ShouldWait: req.Wait, + CleanupOnFail: req.CleanupOnFail, + }); err != nil { + return err + } + } else { + manifests := relutil.GroupManifestsByWeight(relutil.SplitManifestContent(target.Manifest)) + for _, manifest := range manifests { + c = bytes.NewBufferString(current.Manifest) + t = bytes.NewBufferString(manifest) + if err = env.KubeClient.UpdateWithOptions(target.Namespace, c, t, kube.UpdateOptions{ + Force: req.Force, + Recreate: req.Recreate, + Timeout: req.Timeout, + ShouldWait: req.Wait, + CleanupOnFail: req.CleanupOnFail, + }); err != nil { + return err + } + } + } + + c = bytes.NewBufferString(current.Manifest) + t = bytes.NewBufferString(target.Manifest) + if err = env.KubeClient.RemoveDiff(target.Namespace, c, t); err != nil { + return err + } + + return nil } // Status returns kubectl-like formatted status of release objects @@ -161,7 +235,7 @@ func (m *RemoteReleaseModule) Delete(r *release.Release, req *services.Uninstall // DeleteRelease is a helper that allows Rudder to delete a release without exposing most of Tiller inner functions func DeleteRelease(rel *release.Release, vs chartutil.VersionSet, kubeClient environment.KubeClient) (kept string, errs []error) { manifests := relutil.SplitManifests(rel.Manifest) - _, files, err := sortManifests(manifests, vs, UninstallOrder) + _, files, err := sortManifests(rel.Chart, manifests, vs, SortUninstall) if err != nil { // We could instead just delete everything in no particular order. // FIXME: One way to delete at this point would be to try a label-based diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go index b174a32cf..fc13e7526 100644 --- a/pkg/tiller/release_server.go +++ b/pkg/tiller/release_server.go @@ -383,7 +383,7 @@ func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values // Sort hooks, manifests, and partials. Only hooks and manifests are returned, // as partials are not used after renderer.Render. Empty manifests are also // removed here. - hooks, manifests, err := sortManifests(files, vs, InstallOrder) + hooks, manifests, err := sortManifests(ch, files, vs, SortInstall) if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. @@ -405,6 +405,7 @@ func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values b := bytes.NewBuffer(nil) for _, m := range manifests { b.WriteString("\n---\n# Source: " + m.Name + "\n") + b.WriteString(fmt.Sprintf("# Weight: %d,%d\n", m.Weight.Chart, m.Weight.Manifest)) b.WriteString(m.Content) } diff --git a/pkg/tiller/release_server_test.go b/pkg/tiller/release_server_test.go index a383add91..b3c9f1bc2 100644 --- a/pkg/tiller/release_server_test.go +++ b/pkg/tiller/release_server_test.go @@ -665,6 +665,9 @@ func (kc *mockHooksKubeClient) Update(ns string, currentReader, modifiedReader i func (kc *mockHooksKubeClient) UpdateWithOptions(ns string, currentReader, modifiedReader io.Reader, opts kube.UpdateOptions) error { return nil } +func (kc *mockHooksKubeClient) RemoveDiff(namespace string, originalReader, targetReader io.Reader) error { + return nil +} func (kc *mockHooksKubeClient) Build(ns string, reader io.Reader) (kube.Result, error) { return []*resource.Info{}, nil } diff --git a/pkg/tiller/release_update_test.go b/pkg/tiller/release_update_test.go index e47e526d6..5e5bdb340 100644 --- a/pkg/tiller/release_update_test.go +++ b/pkg/tiller/release_update_test.go @@ -91,7 +91,7 @@ func TestUpdateRelease(t *testing.T) { t.Errorf("Expected release values %q, got %q", rel.Config.Raw, res.Release.Config.Raw) } - if !strings.Contains(updated.Manifest, "---\n# Source: hello/templates/hello\nhello: world") { + if !strings.Contains(updated.Manifest, "---\n# Source: hello/templates/hello\n# Weight: 0,0\nhello: world") { t.Errorf("unexpected output: %s", updated.Manifest) } diff --git a/pkg/tiller/weight_sorter.go b/pkg/tiller/weight_sorter.go new file mode 100644 index 000000000..80674ae5d --- /dev/null +++ b/pkg/tiller/weight_sorter.go @@ -0,0 +1,56 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tiller + +import ( + "sort" +) + +// sortByWeight does an in-place sort of manifests by Kind. +// +// Results are sorted by weight +func sortByWeight(manifests []Manifest, st SortType) []Manifest { + ws := newWeightSorter(manifests, st) + sort.Stable(ws) + return ws.manifests +} + +type weightSorter struct { + manifests []Manifest + stype SortType +} + +func newWeightSorter(m []Manifest, t SortType) *weightSorter { + return &weightSorter{ + manifests: m, + stype: t, + } +} + +func (w *weightSorter) Len() int { return len(w.manifests) } + +func (w *weightSorter) Swap(i, j int) { w.manifests[i], w.manifests[j] = w.manifests[j], w.manifests[i] } + +func (w *weightSorter) Less(i, j int) bool { + a := w.manifests[i] + b := w.manifests[j] + + if w.stype == SortInstall { + return a.Weight.GreaterThan(b.Weight) + } + return a.Weight.LessThan(b.Weight) +} diff --git a/pkg/tiller/weight_sorter_test.go b/pkg/tiller/weight_sorter_test.go new file mode 100644 index 000000000..7345e0f6b --- /dev/null +++ b/pkg/tiller/weight_sorter_test.go @@ -0,0 +1,108 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tiller + +import ( + "bytes" + "testing" + + "k8s.io/helm/pkg/manifest" +) + +func TestWeightSorter(t *testing.T) { + manifests := []Manifest{ + { + Name: "a", + Weight: &manifest.Weight{ + Chart: 1, + Manifest: 5, + }, + }, + { + Name: "u", + Weight: &manifest.Weight{ + Chart: 0, + Manifest: 2, + }, + }, + { + Name: "d", + Weight: &manifest.Weight{ + Chart: 1, + Manifest: 0, + }, + }, + { + Name: "e", + Weight: &manifest.Weight{ + Chart: 2, + Manifest: 0, + }, + }, + { + Name: "t", + Weight: &manifest.Weight{ + Chart: 10, + Manifest: 4294967295, + }, + }, + { + Name: "b", + Weight: &manifest.Weight{ + Chart: 0, + Manifest: 0, + }, + }, + { + Name: "p", + Weight: &manifest.Weight{ + Chart: 1, + Manifest: 5, + }, + }, + { + Name: "s", + Weight: &manifest.Weight{ + Chart: 10, + Manifest: 10, + }, + }, + } + + for _, test := range []struct { + description string + order SortType + expected string + }{ + {"install", SortInstall, "tseapdub"}, + {"uninstall", SortUninstall, "budapest"}, + } { + var buf bytes.Buffer + t.Run(test.description, func(t *testing.T) { + if got, want := len(test.expected), len(manifests); got != want { + t.Fatalf("Expected %d names in order, got %d", want, got) + } + defer buf.Reset() + for _, r := range sortByWeight(manifests, test.order) { + buf.WriteString(r.Name) + } + if got := buf.String(); got != test.expected { + t.Errorf("Expected %q, got %q", test.expected, got) + } + }) + } +}