diff --git a/.github/workflows/golang-ci-lint.yml b/.github/workflows/golang-ci-lint.yml index 01ef830d..0e12e1e1 100644 --- a/.github/workflows/golang-ci-lint.yml +++ b/.github/workflows/golang-ci-lint.yml @@ -13,11 +13,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + - uses: actions/setup-go@v3 + with: + go-version: '1.17.7' - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: v1.43.0 + skip-go-installation: true # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/Makefile b/Makefile index b51dd4fb..cd9f41ec 100644 --- a/Makefile +++ b/Makefile @@ -131,6 +131,7 @@ gen-yaml: kustomize build ./install/sample/overlays/rollout-bluegreen > ./out/yaml/sample-greeting-rollout-bluegreen.yaml kustomize build ./install/sample/overlays/remote > ./out/yaml/remotecluster_sample.yaml cp ./install/sample/sample_dep.yaml ./out/yaml/sample_dep.yaml + cp ./install/sample/greeting_preview.yaml ./out/yaml/greeting_preview.yaml cp ./install/sample/gtp.yaml ./out/yaml/gtp.yaml cp ./install/sample/gtp_failover.yaml ./out/yaml/gtp_failover.yaml cp ./install/sample/gtp_topology.yaml ./out/yaml/gtp_topology.yaml diff --git a/admiral/pkg/clusters/handler.go b/admiral/pkg/clusters/handler.go index f6b608f3..4fab356f 100644 --- a/admiral/pkg/clusters/handler.go +++ b/admiral/pkg/clusters/handler.go @@ -747,7 +747,7 @@ func copyEndpoint(e *v1alpha32.ServiceEntry_Endpoint) *v1alpha32.ServiceEntry_En // A rollout can use one of 2 stratergies :- // 1. Canary strategy - which can use a virtual service to manage the weights associated with a stable and canary service. Admiral created endpoints in service entries will use the weights assigned in the Virtual Service -// 2. Blue green strategy- this contains 2 service instances in a namespace, an active service and a preview service. Admiral will always use the active service +// 2. Blue green strategy- this contains 2 service instances in a namespace, an active service and a preview service. Admiral will use repective service to create active and preview endpoints func getServiceForRollout(rc *RemoteController, rollout *argo.Rollout) map[string]*WeightedService { if rollout == nil { @@ -769,9 +769,12 @@ func getServiceForRollout(rc *RemoteController, rollout *argo.Rollout) map[strin var istioCanaryWeights = make(map[string]int32) var blueGreenActiveService string + var blueGreenPreviewService string + if rolloutStrategy.BlueGreen != nil { - // If rollout uses blue green strategy, use the active service + // If rollout uses blue green strategy blueGreenActiveService = rolloutStrategy.BlueGreen.ActiveService + blueGreenPreviewService = rolloutStrategy.BlueGreen.PreviewService } else if rolloutStrategy.Canary != nil && rolloutStrategy.Canary.TrafficRouting != nil && rolloutStrategy.Canary.TrafficRouting.Istio != nil { canaryService = rolloutStrategy.Canary.CanaryService stableService = rolloutStrategy.Canary.StableService @@ -844,7 +847,7 @@ func getServiceForRollout(rc *RemoteController, rollout *argo.Rollout) map[strin var service = servicesInNamespace[s] var match = true //skip services that are not referenced in the rollout - if len(blueGreenActiveService) > 0 && service.ObjectMeta.Name != blueGreenActiveService { + if len(blueGreenActiveService) > 0 && service.ObjectMeta.Name != blueGreenActiveService && service.ObjectMeta.Name != blueGreenPreviewService { log.Infof("Skipping service=%s for rollout=%s in namespace=%s and cluster=%s", service.Name, rollout.Name, rollout.Namespace, rc.ClusterID) continue } @@ -865,8 +868,11 @@ func getServiceForRollout(rc *RemoteController, rollout *argo.Rollout) map[strin if match { ports := GetMeshPortsForRollout(rc.ClusterID, service, rollout) if len(ports) > 0 { - //if the strategy is bluegreen or using canary with NO istio traffic management, pick the first service that matches - if len(istioCanaryWeights) == 0 { + // if the strategy is bluegreen return matched services + // else if using canary with NO istio traffic management, pick the first service that matches + if rolloutStrategy.BlueGreen != nil { + matchedServices[service.Name] = &WeightedService{Weight: 1, Service: service} + } else if len(istioCanaryWeights) == 0 { matchedServices[service.Name] = &WeightedService{Weight: 1, Service: service} break } diff --git a/admiral/pkg/clusters/serviceentry.go b/admiral/pkg/clusters/serviceentry.go index 3f53dd6d..97bf7b8c 100644 --- a/admiral/pkg/clusters/serviceentry.go +++ b/admiral/pkg/clusters/serviceentry.go @@ -62,6 +62,7 @@ func modifyServiceEntryForNewServiceOrPod(event admiral.EventType, env string, s var serviceEntries = make(map[string]*networking.ServiceEntry) var cname string + cnames := make(map[string]string) var serviceInstance *k8sV1.Service var weightedServices map[string]*WeightedService var rollout *admiral.RolloutClusterEntry @@ -104,6 +105,7 @@ func modifyServiceEntryForNewServiceOrPod(event admiral.EventType, env string, s localMeshPorts := GetMeshPortsForRollout(rc.ClusterID, serviceInstance, rolloutInstance) cname = common.GetCnameForRollout(rolloutInstance, common.GetWorkloadIdentifier(), common.GetHostnameSuffix()) + cnames[cname] = "1" sourceRollouts[rc.ClusterID] = rolloutInstance createServiceEntryForRollout(event, rc, remoteRegistry.AdmiralCache, localMeshPorts, rolloutInstance, serviceEntries) } else { @@ -132,10 +134,14 @@ func modifyServiceEntryForNewServiceOrPod(event admiral.EventType, env string, s localFqdn := serviceInstance.Name + common.Sep + serviceInstance.Namespace + common.DotLocalDomainSuffix rc := remoteRegistry.RemoteControllers[sourceCluster] var meshPorts map[string]uint32 + isBlueGreenStrategy := false + + if len(sourceRollouts) > 0 { + isBlueGreenStrategy = sourceRollouts[sourceCluster].Spec.Strategy.BlueGreen != nil + } + if len(sourceDeployments) > 0 { meshPorts = GetMeshPorts(sourceCluster, serviceInstance, sourceDeployments[sourceCluster]) - } else { - meshPorts = GetMeshPortsForRollout(sourceCluster, serviceInstance, sourceRollouts[sourceCluster]) } for key, serviceEntry := range serviceEntries { @@ -147,9 +153,20 @@ func modifyServiceEntryForNewServiceOrPod(event admiral.EventType, env string, s for _, ep := range serviceEntry.Endpoints { //replace istio ingress-gateway address with local fqdn, note that ingress-gateway can be empty (not provisoned, or is not up) if ep.Address == clusterIngress || ep.Address == "" { - // see if we have weighted services (rollouts with canary strategy) - if len(weightedServices) > 1 { + // Update endpoints with locafqdn for active and preview se of bluegreen rollout + if isBlueGreenStrategy { + oldPorts := ep.Ports + updateEndpointsForBlueGreen(sourceRollouts[sourceCluster], weightedServices, cnames, ep, sourceCluster, key) + + AddServiceEntriesWithDr(remoteRegistry.AdmiralCache, map[string]string{sourceCluster: sourceCluster}, remoteRegistry.RemoteControllers, + map[string]*networking.ServiceEntry{key: serviceEntry}) + //swap it back to use for next iteration + ep.Address = clusterIngress + ep.Ports = oldPorts + // see if we have weighted services (rollouts with canary strategy) + } else if len(weightedServices) > 1 { //add one endpoint per each service, may be modify + meshPorts = GetMeshPortsForRollout(sourceCluster, serviceInstance, sourceRollouts[sourceCluster]) var se = copyServiceEntry(serviceEntry) updateEndpointsForWeightedServices(se, weightedServices, clusterIngress, meshPorts) AddServiceEntriesWithDr(remoteRegistry.AdmiralCache, map[string]string{sourceCluster: sourceCluster}, remoteRegistry.RemoteControllers, @@ -169,7 +186,7 @@ func modifyServiceEntryForNewServiceOrPod(event admiral.EventType, env string, s } for _, val := range dependents { - remoteRegistry.AdmiralCache.DependencyNamespaceCache.Put(val, serviceInstance.Namespace, localFqdn, map[string]string{cname: "1"}) + remoteRegistry.AdmiralCache.DependencyNamespaceCache.Put(val, serviceInstance.Namespace, localFqdn, cnames) } if common.GetWorkloadSidecarUpdate() == "enabled" { @@ -179,6 +196,26 @@ func modifyServiceEntryForNewServiceOrPod(event admiral.EventType, env string, s return serviceEntries } +func updateEndpointsForBlueGreen(rollout *argo.Rollout, weightedServices map[string]*WeightedService, cnames map[string]string, + ep *networking.ServiceEntry_Endpoint, sourceCluster string, meshHost string) { + activeServiceName := rollout.Spec.Strategy.BlueGreen.ActiveService + previewServiceName := rollout.Spec.Strategy.BlueGreen.PreviewService + + if previewService, ok := weightedServices[previewServiceName]; strings.HasPrefix(meshHost, common.BlueGreenRolloutPreviewPrefix+common.Sep) && ok { + previewServiceInstance := previewService.Service + localFqdn := previewServiceInstance.Name + common.Sep + previewServiceInstance.Namespace + common.DotLocalDomainSuffix + cnames[localFqdn] = "1" + ep.Address = localFqdn + ep.Ports = GetMeshPortsForRollout(sourceCluster, previewServiceInstance, rollout) + } else if activeService, ok := weightedServices[activeServiceName]; ok { + activeServiceInstance := activeService.Service + localFqdn := activeServiceInstance.Name + common.Sep + activeServiceInstance.Namespace + common.DotLocalDomainSuffix + cnames[localFqdn] = "1" + ep.Address = localFqdn + ep.Ports = GetMeshPortsForRollout(sourceCluster, activeServiceInstance, rollout) + } +} + //update endpoints for Argo rollouts specific Service Entries to account for traffic splitting (Canary strategy) func updateEndpointsForWeightedServices(serviceEntry *networking.ServiceEntry, weightedServices map[string]*WeightedService, clusterIngress string, meshPorts map[string]uint32) { var endpoints = make([]*networking.ServiceEntry_Endpoint, 0) @@ -570,6 +607,17 @@ func createServiceEntryForRollout(event admiral.EventType, rc *RemoteController, san := getSanForRollout(destRollout, workloadIdentityKey) + if destRollout.Spec.Strategy.BlueGreen != nil && destRollout.Spec.Strategy.BlueGreen.PreviewService != "" { + rolloutServices := getServiceForRollout(rc, destRollout) + if _, ok := rolloutServices[destRollout.Spec.Strategy.BlueGreen.PreviewService]; ok { + previewGlobalFqdn := common.BlueGreenRolloutPreviewPrefix + common.Sep + common.GetCnameForRollout(destRollout, workloadIdentityKey, common.GetHostnameSuffix()) + previewAddress := getUniqueAddress(admiralCache, previewGlobalFqdn) + if len(previewGlobalFqdn) != 0 && len(previewAddress) != 0 { + generateServiceEntry(event, admiralCache, meshPorts, previewGlobalFqdn, rc, serviceEntries, previewAddress, san) + } + } + } + tmpSe := generateServiceEntry(event, admiralCache, meshPorts, globalFqdn, rc, serviceEntries, address, san) return tmpSe } diff --git a/admiral/pkg/clusters/serviceentry_test.go b/admiral/pkg/clusters/serviceentry_test.go index e4d1b5e4..9a8387a0 100644 --- a/admiral/pkg/clusters/serviceentry_test.go +++ b/admiral/pkg/clusters/serviceentry_test.go @@ -3,6 +3,14 @@ package clusters import ( "context" "errors" + "reflect" + "strconv" + "strings" + "sync" + "testing" + "time" + "unicode" + argo "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" "github.com/google/go-cmp/cmp" "github.com/istio-ecosystem/admiral/admiral/pkg/apis/admiral/model" @@ -17,17 +25,10 @@ import ( "istio.io/client-go/pkg/apis/networking/v1alpha3" istiofake "istio.io/client-go/pkg/clientset/versioned/fake" v14 "k8s.io/api/apps/v1" - "k8s.io/api/core/v1" coreV1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" v12 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/rest" - "reflect" - "strconv" - "strings" - "sync" - "testing" - "time" - "unicode" ) func TestCreateSeWithDrLabels(t *testing.T) { @@ -261,7 +262,6 @@ func TestCreateSeAndDrSetFromGtp(t *testing.T) { seDrSet: map[string]*SeDrTuple{host: nil, common.GetCnameVal([]string{west, host}): nil, strings.ToLower(common.GetCnameVal([]string{eastWithCaps, host})): nil}, }, - } //Run the test for every provided case @@ -994,6 +994,250 @@ func TestCreateServiceEntryForNewServiceOrPodRolloutsUsecase(t *testing.T) { } } +func TestCreateServiceEntryForBlueGreenRolloutsUsecase(t *testing.T) { + + const NAMESPACE = "test-test" + const ACTIVE_SERVICENAME = "serviceNameActive" + const PREVIEW_SERVICENAME = "serviceNamePreview" + const ROLLOUT_POD_HASH_LABEL string = "rollouts-pod-template-hash" + + p := common.AdmiralParams{ + KubeconfigPath: "testdata/fake.config", + PreviewHostnamePrefix: "preview", + } + rr, _ := InitAdmiral(context.Background(), p) + config := rest.Config{ + Host: "localhost", + } + + d, e := admiral.NewDeploymentController(make(chan struct{}), &test.MockDeploymentHandler{}, &config, time.Second*time.Duration(300)) + + r, e := admiral.NewRolloutsController(make(chan struct{}), &test.MockRolloutHandler{}, &config, time.Second*time.Duration(300)) + v, e := istio.NewVirtualServiceController(make(chan struct{}), &test.MockVirtualServiceHandler{}, &config, time.Second*time.Duration(300)) + + if e != nil { + t.Fail() + } + s, e := admiral.NewServiceController(make(chan struct{}), &test.MockServiceHandler{}, &config, time.Second*time.Duration(300)) + + cacheWithEntry := ServiceEntryAddressStore{ + EntryAddresses: map[string]string{ + "test.test.mesh-se": common.LocalAddressPrefix + ".10.1", + "preview.test.test.mesh-se": common.LocalAddressPrefix + ".10.2", + }, + Addresses: []string{common.LocalAddressPrefix + ".10.1", common.LocalAddressPrefix + ".10.2"}, + } + + fakeIstioClient := istiofake.NewSimpleClientset() + rc := &RemoteController{ + ServiceEntryController: &istio.ServiceEntryController{ + IstioClient: fakeIstioClient, + }, + DestinationRuleController: &istio.DestinationRuleController{ + IstioClient: fakeIstioClient, + }, + NodeController: &admiral.NodeController{ + Locality: &admiral.Locality{ + Region: "us-west-2", + }, + }, + DeploymentController: d, + RolloutController: r, + ServiceController: s, + VirtualServiceController: v, + } + rc.ClusterID = "test.cluster" + rr.RemoteControllers["test.cluster"] = rc + + admiralCache := &AdmiralCache{ + IdentityClusterCache: common.NewMapOfMaps(), + ServiceEntryAddressStore: &cacheWithEntry, + CnameClusterCache: common.NewMapOfMaps(), + CnameIdentityCache: &sync.Map{}, + CnameDependentClusterCache: common.NewMapOfMaps(), + IdentityDependencyCache: common.NewMapOfMaps(), + GlobalTrafficCache: &globalTrafficCache{}, + DependencyNamespaceCache: common.NewSidecarEgressMap(), + SeClusterCache: common.NewMapOfMaps(), + } + rr.AdmiralCache = admiralCache + + rollout := argo.Rollout{} + + rollout.Spec = argo.RolloutSpec{ + Template: coreV1.PodTemplateSpec{ + ObjectMeta: v12.ObjectMeta{ + Labels: map[string]string{"identity": "test"}, + }, + }, + } + + rollout.Namespace = NAMESPACE + rollout.Spec.Strategy = argo.RolloutStrategy{ + BlueGreen: &argo.BlueGreenStrategy{ActiveService: ACTIVE_SERVICENAME, PreviewService: PREVIEW_SERVICENAME}, + } + labelMap := make(map[string]string) + labelMap["identity"] = "test" + + matchLabel4 := make(map[string]string) + matchLabel4["app"] = "test" + + labelSelector4 := v12.LabelSelector{ + MatchLabels: matchLabel4, + } + rollout.Spec.Selector = &labelSelector4 + + r.Cache.UpdateRolloutToClusterCache("bar", &rollout) + + selectorMap := make(map[string]string) + selectorMap["app"] = "test" + selectorMap[ROLLOUT_POD_HASH_LABEL] = "hash" + + port1 := coreV1.ServicePort{ + Port: 8080, + Name: "random1", + } + + port2 := coreV1.ServicePort{ + Port: 8081, + Name: "random2", + } + + ports := []coreV1.ServicePort{port1, port2} + + activeService := &coreV1.Service{ + Spec: coreV1.ServiceSpec{ + Selector: selectorMap, + }, + } + activeService.Name = ACTIVE_SERVICENAME + activeService.Namespace = NAMESPACE + activeService.Spec.Ports = ports + + s.Cache.Put(activeService) + + previewService := &coreV1.Service{ + Spec: coreV1.ServiceSpec{ + Selector: selectorMap, + }, + } + previewService.Name = PREVIEW_SERVICENAME + previewService.Namespace = NAMESPACE + previewService.Spec.Ports = ports + + s.Cache.Put(previewService) + + se := modifyServiceEntryForNewServiceOrPod(admiral.Add, "test", "bar", rr) + + if nil == se { + t.Fatalf("no service entries found") + } + if len(se) != 2 { + t.Fatalf("Expected 2 service entries to be created but found %d", len(se)) + } + serviceEntryResp := se["test.test.mesh"] + if nil == serviceEntryResp { + t.Fatalf("Service entry returned should not be empty") + } + previewServiceEntryResp := se["preview.test.test.mesh"] + if nil == previewServiceEntryResp { + t.Fatalf("Preview Service entry returned should not be empty") + } + + // When Preview service is not defined in BlueGreen strategy + rollout.Spec.Strategy = argo.RolloutStrategy{ + BlueGreen: &argo.BlueGreenStrategy{ActiveService: ACTIVE_SERVICENAME}, + } + + se = modifyServiceEntryForNewServiceOrPod(admiral.Add, "test", "bar", rr) + + if len(se) != 1 { + t.Fatalf("Expected 1 service entries to be created but found %d", len(se)) + } + serviceEntryResp = se["test.test.mesh"] + + if nil == serviceEntryResp { + t.Fatalf("Service entry returned should not be empty") + } +} + +func TestUpdateEndpointsForBlueGreen(t *testing.T) { + const CLUSTER_INGRESS_1 = "ingress1.com" + const ACTIVE_SERVICE = "activeService" + const PREVIEW_SERVICE = "previewService" + const NAMESPACE = "namespace" + const ACTIVE_MESH_HOST = "qal.example.mesh" + const PREVIEW_MESH_HOST = "preview.qal.example.mesh" + + rollout := argo.Rollout{} + rollout.Spec.Strategy = argo.RolloutStrategy{ + BlueGreen: &argo.BlueGreenStrategy{ + ActiveService: ACTIVE_SERVICE, + PreviewService: PREVIEW_SERVICE, + }, + } + rollout.Spec.Template.Annotations = map[string]string{} + rollout.Spec.Template.Annotations[common.SidecarEnabledPorts] = "8080" + + endpoint := istionetworkingv1alpha3.ServiceEntry_Endpoint{ + Labels: map[string]string{}, Address: CLUSTER_INGRESS_1, Ports: map[string]uint32{"http": 15443}, + } + + meshPorts := map[string]uint32{"http": 8080} + + weightedServices := map[string]*WeightedService{ + ACTIVE_SERVICE: {Service: &v1.Service{ObjectMeta: v12.ObjectMeta{Name: ACTIVE_SERVICE, Namespace: NAMESPACE}}}, + PREVIEW_SERVICE: {Service: &v1.Service{ObjectMeta: v12.ObjectMeta{Name: PREVIEW_SERVICE, Namespace: NAMESPACE}}}, + } + + activeWantedEndpoints := istionetworkingv1alpha3.ServiceEntry_Endpoint{ + Address: ACTIVE_SERVICE + common.Sep + NAMESPACE + common.DotLocalDomainSuffix, Ports: meshPorts, + } + + previewWantedEndpoints := istionetworkingv1alpha3.ServiceEntry_Endpoint{ + Address: PREVIEW_SERVICE + common.Sep + NAMESPACE + common.DotLocalDomainSuffix, Ports: meshPorts, + } + + testCases := []struct { + name string + rollout argo.Rollout + inputEndpoint istionetworkingv1alpha3.ServiceEntry_Endpoint + weightedServices map[string]*WeightedService + clusterIngress string + meshPorts map[string]uint32 + meshHost string + wantedEndpoints istionetworkingv1alpha3.ServiceEntry_Endpoint + }{ + { + name: "should return endpoint with active service address", + rollout: rollout, + inputEndpoint: endpoint, + weightedServices: weightedServices, + meshPorts: meshPorts, + meshHost: ACTIVE_MESH_HOST, + wantedEndpoints: activeWantedEndpoints, + }, + { + name: "should return endpoint with preview service address", + rollout: rollout, + inputEndpoint: endpoint, + weightedServices: weightedServices, + meshPorts: meshPorts, + meshHost: PREVIEW_MESH_HOST, + wantedEndpoints: previewWantedEndpoints, + }, + } + + for _, c := range testCases { + t.Run(c.name, func(t *testing.T) { + updateEndpointsForBlueGreen(&c.rollout, c.weightedServices, map[string]string{}, &c.inputEndpoint, "test", c.meshHost) + if c.inputEndpoint.Address != c.wantedEndpoints.Address { + t.Errorf("Wanted %s endpoint, got: %s", c.wantedEndpoints.Address, c.inputEndpoint.Address) + } + }) + } +} + func TestUpdateEndpointsForWeightedServices(t *testing.T) { t.Parallel() @@ -1001,12 +1245,12 @@ func TestUpdateEndpointsForWeightedServices(t *testing.T) { const CLUSTER_INGRESS_2 = "ingress2.com" const CANARY_SERVICE = "canaryService" const STABLE_SERVICE = "stableService" - const NAMESPACE = "namespace" + const NAMESPACE = "namespace" se := &istionetworkingv1alpha3.ServiceEntry{ - Endpoints: []*istionetworkingv1alpha3.ServiceEntry_Endpoint{ - {Labels: map[string]string{}, Address: CLUSTER_INGRESS_1, Weight: 10, Ports: map[string]uint32{"http" : 15443,}}, - {Labels: map[string]string{}, Address: CLUSTER_INGRESS_2, Weight: 10, Ports: map[string]uint32{"http" : 15443}}, + Endpoints: []*istionetworkingv1alpha3.ServiceEntry_Endpoint{ + {Labels: map[string]string{}, Address: CLUSTER_INGRESS_1, Weight: 10, Ports: map[string]uint32{"http": 15443}}, + {Labels: map[string]string{}, Address: CLUSTER_INGRESS_2, Weight: 10, Ports: map[string]uint32{"http": 15443}}, }, } @@ -1022,47 +1266,47 @@ func TestUpdateEndpointsForWeightedServices(t *testing.T) { } wantedEndpoints := []*istionetworkingv1alpha3.ServiceEntry_Endpoint{ - {Address: CLUSTER_INGRESS_2, Weight: 10, Ports: map[string]uint32{"http" : 15443}}, + {Address: CLUSTER_INGRESS_2, Weight: 10, Ports: map[string]uint32{"http": 15443}}, {Address: STABLE_SERVICE + common.Sep + NAMESPACE + common.DotLocalDomainSuffix, Weight: 90, Ports: meshPorts}, {Address: CANARY_SERVICE + common.Sep + NAMESPACE + common.DotLocalDomainSuffix, Weight: 10, Ports: meshPorts}, } wantedEndpointsZeroWeights := []*istionetworkingv1alpha3.ServiceEntry_Endpoint{ - {Address: CLUSTER_INGRESS_2, Weight: 10, Ports: map[string]uint32{"http" : 15443}}, + {Address: CLUSTER_INGRESS_2, Weight: 10, Ports: map[string]uint32{"http": 15443}}, {Address: STABLE_SERVICE + common.Sep + NAMESPACE + common.DotLocalDomainSuffix, Weight: 100, Ports: meshPorts}, } testCases := []struct { - name string - inputServiceEntry *istionetworkingv1alpha3.ServiceEntry - weightedServices map[string]*WeightedService - clusterIngress string - meshPorts map[string]uint32 - wantedEndpoints []*istionetworkingv1alpha3.ServiceEntry_Endpoint + name string + inputServiceEntry *istionetworkingv1alpha3.ServiceEntry + weightedServices map[string]*WeightedService + clusterIngress string + meshPorts map[string]uint32 + wantedEndpoints []*istionetworkingv1alpha3.ServiceEntry_Endpoint }{ { - name: "should return endpoints with assigned weights", + name: "should return endpoints with assigned weights", inputServiceEntry: copyServiceEntry(se), - weightedServices: weightedServices, - clusterIngress: CLUSTER_INGRESS_1, - meshPorts: meshPorts, - wantedEndpoints: wantedEndpoints, + weightedServices: weightedServices, + clusterIngress: CLUSTER_INGRESS_1, + meshPorts: meshPorts, + wantedEndpoints: wantedEndpoints, }, { - name: "should return endpoints as is", + name: "should return endpoints as is", inputServiceEntry: copyServiceEntry(se), - weightedServices: weightedServices, - clusterIngress: "random", - meshPorts: meshPorts, - wantedEndpoints: copyServiceEntry(se).Endpoints, + weightedServices: weightedServices, + clusterIngress: "random", + meshPorts: meshPorts, + wantedEndpoints: copyServiceEntry(se).Endpoints, }, { - name: "should not return endpoints with zero weight", + name: "should not return endpoints with zero weight", inputServiceEntry: copyServiceEntry(se), - weightedServices: weightedServicesZeroWeight, - clusterIngress: CLUSTER_INGRESS_1, - meshPorts: meshPorts, - wantedEndpoints: wantedEndpointsZeroWeights, + weightedServices: weightedServicesZeroWeight, + clusterIngress: CLUSTER_INGRESS_1, + meshPorts: meshPorts, + wantedEndpoints: wantedEndpointsZeroWeights, }, } diff --git a/admiral/pkg/controller/common/common.go b/admiral/pkg/controller/common/common.go index f0b2ba29..77aa2e35 100644 --- a/admiral/pkg/controller/common/common.go +++ b/admiral/pkg/controller/common/common.go @@ -1,37 +1,39 @@ package common import ( - "github.com/istio-ecosystem/admiral/admiral/pkg/apis/admiral/v1" + "sort" + "strings" + + v1 "github.com/istio-ecosystem/admiral/admiral/pkg/apis/admiral/v1" log "github.com/sirupsen/logrus" k8sAppsV1 "k8s.io/api/apps/v1" k8sV1 "k8s.io/api/core/v1" - "sort" - "strings" ) const ( - NamespaceKubeSystem = "kube-system" - NamespaceIstioSystem = "istio-system" - Env = "env" - Http = "http" - Grpc = "grpc" - GrpcWeb = "grpc-web" - Http2 = "http2" - DefaultMtlsPort = 15443 - DefaultServiceEntryPort = 80 - Sep = "." - Dash = "-" - Slash = "/" - DotLocalDomainSuffix = ".svc.cluster.local" - Mesh = "mesh" - MulticlusterIngressGateway = "istio-multicluster-ingressgateway" - LocalAddressPrefix = "240.0" - NodeRegionLabel = "failure-domain.beta.kubernetes.io/region" - SpiffePrefix = "spiffe://" - SidecarEnabledPorts = "traffic.sidecar.istio.io/includeInboundPorts" - Default = "default" - AdmiralIgnoreAnnotation = "admiral.io/ignore" - AdmiralCnameCaseSensitive = "admiral.io/cname-case-sensitive" + NamespaceKubeSystem = "kube-system" + NamespaceIstioSystem = "istio-system" + Env = "env" + Http = "http" + Grpc = "grpc" + GrpcWeb = "grpc-web" + Http2 = "http2" + DefaultMtlsPort = 15443 + DefaultServiceEntryPort = 80 + Sep = "." + Dash = "-" + Slash = "/" + DotLocalDomainSuffix = ".svc.cluster.local" + Mesh = "mesh" + MulticlusterIngressGateway = "istio-multicluster-ingressgateway" + LocalAddressPrefix = "240.0" + NodeRegionLabel = "failure-domain.beta.kubernetes.io/region" + SpiffePrefix = "spiffe://" + SidecarEnabledPorts = "traffic.sidecar.istio.io/includeInboundPorts" + Default = "default" + AdmiralIgnoreAnnotation = "admiral.io/ignore" + AdmiralCnameCaseSensitive = "admiral.io/cname-case-sensitive" + BlueGreenRolloutPreviewPrefix = "preview" ) type Event int @@ -87,7 +89,7 @@ func GetCname(deployment *k8sAppsV1.Deployment, identifier string, nameSuffix st return strings.ToLower(cname) } -func GetCnameVal(vals[] string) string { +func GetCnameVal(vals []string) string { return strings.Join(vals, Sep) } @@ -250,4 +252,4 @@ func GetGtpEnv(gtp *v1.GlobalTrafficPolicy) string { environment = Default } return environment -} \ No newline at end of file +} diff --git a/admiral/pkg/controller/common/types.go b/admiral/pkg/controller/common/types.go index 8f4e4c5e..df3960b8 100644 --- a/admiral/pkg/controller/common/types.go +++ b/admiral/pkg/controller/common/types.go @@ -41,6 +41,7 @@ type AdmiralParams struct { LabelSet *LabelSet LogLevel int HostnameSuffix string + PreviewHostnamePrefix string WorkloadSidecarUpdate string WorkloadSidecarName string } diff --git a/install/sample/greeting_preview.yaml b/install/sample/greeting_preview.yaml new file mode 100644 index 00000000..c9b25215 --- /dev/null +++ b/install/sample/greeting_preview.yaml @@ -0,0 +1,99 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nginx-conf-preview + namespace: sample-rollout-bluegreen +data: + nginx.conf: | + user nginx; + worker_processes 3; + error_log /var/log/nginx/error.log; + events { + worker_connections 10240; + } + http { + log_format main + 'remote_addr:$remote_addr\t' + 'time_local:$time_local\t' + 'method:$request_method\t' + 'uri:$request_uri\t' + 'host:$host\t' + 'status:$status\t' + 'bytes_sent:$body_bytes_sent\t' + 'referer:$http_referer\t' + 'useragent:$http_user_agent\t' + 'forwardedfor:$http_x_forwarded_for\t' + 'request_time:$request_time'; + access_log /var/log/nginx/access.log main; + server { + listen 80; + server_name _; + location / { + return 200 "Hello World! - Admiral Preview!!"; + + } + } + } +--- +apiVersion: argoproj.io/v1alpha1 +kind: Rollout +metadata: + name: greeting + namespace: sample-rollout-bluegreen + labels: + identity: greeting +spec: + replicas: 1 + selector: + matchLabels: + app: greeting + template: + metadata: + annotations: + admiral.io/env: stage + sidecar.istio.io/inject: "true" + labels: + app: greeting + identity: greeting.bluegreen + spec: + containers: + - image: nginx + name: greeting + ports: + - containerPort: 80 + volumeMounts: + - mountPath: /etc/nginx + name: nginx-conf + readOnly: true + - mountPath: /var/log/nginx + name: log + resources: + requests: + cpu: "10m" + memory: "50Mi" + limits: + cpu: "20m" + memory: "75Mi" + volumes: + - configMap: + items: + - key: nginx.conf + path: nginx.conf + name: nginx-conf-preview + name: nginx-conf + - emptyDir: {} + name: log + strategy: + blueGreen: + # activeService specifies the service to update with the new template hash at time of promotion. + # This field is mandatory for the blueGreen update strategy. + activeService: rollout-bluegreen-active + # previewService specifies the service to update with the new template hash before promotion. + # This allows the preview stack to be reachable without serving production traffic. + # This field is optional. + previewService: rollout-bluegreen-preview + # autoPromotionEnabled disables automated promotion of the new stack by pausing the rollout + # immediately before the promotion. If omitted, the default behavior is to promote the new + # stack as soon as the ReplicaSet are completely ready/available. + # Rollouts can be resumed using: `kubectl argo rollouts resume ROLLOUT` + autoPromotionEnabled: false \ No newline at end of file diff --git a/install/scripts/install_sample_services.sh b/install/scripts/install_sample_services.sh index 0b1bfe12..7ea2f6ed 100755 --- a/install/scripts/install_sample_services.sh +++ b/install/scripts/install_sample_services.sh @@ -30,6 +30,29 @@ kubectl rollout status deployment webapp -n sample kubectl rollout status deployment webapp -n sample-rollout-bluegreen +checkRolloutStatus() { + rolloutName=$1 + namespace=$2 + status=$(kubectl get rollout -n $2 $1 -o jsonpath="{.status.readyReplicas}") + + if [[ "$status" == "1" ]]; then + return 0 + else + echo "Waiting rollout $1 in $2 namespace is not in Running phase $status" + return 1 + fi +} + +export -f checkRolloutStatus + +timeout 180s bash -c "until checkRolloutStatus greeting sample-rollout-bluegreen ; do sleep 10; done" +if [[ $? -eq 124 ]] + then + exit 1 +fi +# Update BlueGreen Rollout with new preview release +kubectl apply -f $install_dir/yaml/greeting_preview.yaml + #Verify that admiral created service names for 'greeting' service checkse() { identity=$1 diff --git a/tests/run.sh b/tests/run.sh index dd67117d..620159f5 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -28,6 +28,7 @@ $install_dir/scripts/install_sample_services.sh $install_dir sleep 10 ./test1.sh "webapp" "sample" "greeting" ./test2.sh "webapp" "sample-rollout-bluegreen" "greeting.bluegreen" +./test5.sh "webapp" "sample-rollout-bluegreen" "greeting.bluegreen" ./test2.sh "webapp" "sample-rollout-canary" "greeting.canary" #cleanup to fee up the pipeline minkube resources if [[ $IS_LOCAL == "false" ]]; then diff --git a/tests/test5.sh b/tests/test5.sh new file mode 100755 index 00000000..0b9bd081 --- /dev/null +++ b/tests/test5.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +[ $# -lt 3 ] && { echo "Usage: $0 " ; exit 1; } + +source=$1 +source_ns=$2 +dest=$3 + +#Test +output=$(kubectl exec --namespace=$source_ns -it $(kubectl get pod -l "app=$source" --namespace=$source_ns -o jsonpath='{.items[0].metadata.name}') -c $source -- curl -v "http://preview.stage.$dest.global" && echo "") + +if [[ "$output" == *"Admiral Preview"* ]]; then + echo "Rollout BlueGreen Preview: PASS" + exit 0 +else + echo "FAIL" . $output + exit 1 +fi