Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add flag that checks whether any of the plans have been deactivated #115

Merged
merged 11 commits into from
Dec 11, 2023
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### Checklist:

* [ ] Have you added Release Notes in the docs respositories?
* [ ] Have you added Release Notes in the docs repositories?
* [ ] Have you followed the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary)?
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ Options:
-parallel - number of upgrades to run in parallel (defaults to 10)
-loghttp - log HTTP requests and responses
-dry-run - print the service instances that would be upgraded
```
-check-up-to-date - checks and fails if any service instance is not up-to-date - implies a dry-run
-check-deactivated-plans - checks whether any of the plans have been deactivated. If any deactivated plans are found, the command will fail
```
79 changes: 28 additions & 51 deletions internal/ccapi/service_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,8 @@ type ServiceInstance struct {
OrganizationGUID string `json:"-"`
OrganizationName string `json:"-"`

// Can't be retrieved from CF API using `fields` query parameter
// We populate this field in Upgrade function in internal/upgrader/upgrader.go
PlanMaintenanceInfoVersion string `json:"-"`
}

type includedPlan struct {
GUID string `json:"guid"`
Name string `json:"name"`
ServiceOfferingGUID string `jsonry:"relationships.service_offering.data.guid"`
ServicePlanMaintenanceInfoVersion string `json:"-"`
ServicePlanDeactivated bool `json:"-"`
}

type includedSpace struct {
Expand All @@ -47,42 +40,34 @@ type includedOrganization struct {
Name string `json:"name"`
}

type includedServiceOffering struct {
GUID string `json:"guid"`
Name string `json:"name"`
}

func BuildQueryParams(planGUIDs []string) string {
return fmt.Sprintf("per_page=5000&fields[space]=name,guid,relationships.organization&fields[space.organization]=name,guid&fields[service_plan]=name,guid,relationships.service_offering&fields[service_plan.service_offering]=guid,name&service_plan_guids=%s", strings.Join(planGUIDs, ","))
return fmt.Sprintf("per_page=5000&fields[space]=name,guid,relationships.organization&fields[space.organization]=name,guid&service_plan_guids=%s", strings.Join(planGUIDs, ","))
}

func (c CCAPI) GetServiceInstances(planGUIDs []string) ([]ServiceInstance, error) {
if len(planGUIDs) == 0 {
return nil, fmt.Errorf("no service_plan_guids specified")
}
func (c CCAPI) GetServiceInstancesForServicePlans(plans []ServicePlan) ([]ServiceInstance, error) {

var receiver struct {
Instances []ServiceInstance `json:"resources"`
Included struct {
Plans []includedPlan `json:"service_plans"`
Spaces []includedSpace `json:"spaces"`
Organizations []includedOrganization `json:"organizations"`
ServiceOfferings []includedServiceOffering `json:"service_offerings"`
Spaces []includedSpace `json:"spaces"`
Organizations []includedOrganization `json:"organizations"`
} `json:"included"`
}

if err := c.requester.Get("v3/service_instances?"+BuildQueryParams(planGUIDs), &receiver); err != nil {
if err := c.requester.Get("v3/service_instances?"+BuildQueryParams(getPlansGUIDs(plans)), &receiver); err != nil {
return nil, fmt.Errorf("error getting service instances: %s", err)
}

// Enrich with service plan, service offering space, and org data
servicePlanGUIDLookup := computeServicePlanGUIDLookup(receiver.Included.Plans, receiver.Included.ServiceOfferings)
spaceGUIDLookup := computeSpaceGUIDLookup(receiver.Included.Spaces, receiver.Included.Organizations)
planGUIDLookup := computePlanGUIDLookup(plans)
for i := range receiver.Instances {
planName, offeringGUID, offeringName := servicePlanGUIDLookup(receiver.Instances[i].ServicePlanGUID)
receiver.Instances[i].ServicePlanName = planName
receiver.Instances[i].ServiceOfferingGUID = offeringGUID
receiver.Instances[i].ServiceOfferingName = offeringName
plan := planGUIDLookup(receiver.Instances[i].ServicePlanGUID)
receiver.Instances[i].ServicePlanName = plan.Name
receiver.Instances[i].ServiceOfferingGUID = plan.ServiceOfferingGUID
receiver.Instances[i].ServiceOfferingName = plan.ServiceOfferingName
receiver.Instances[i].ServicePlanMaintenanceInfoVersion = plan.MaintenanceInfoVersion
receiver.Instances[i].ServicePlanDeactivated = !plan.Available

spaceName, orgGUID, orgName := spaceGUIDLookup(receiver.Instances[i].SpaceGUID)
receiver.Instances[i].SpaceName = spaceName
Expand All @@ -93,29 +78,13 @@ func (c CCAPI) GetServiceInstances(planGUIDs []string) ([]ServiceInstance, error
return receiver.Instances, nil
}

func computeServicePlanGUIDLookup(plans []includedPlan, offerings []includedServiceOffering) func(key string) (string, string, string) {
offeringLookup := make(map[string]string)
for _, o := range offerings {
offeringLookup[o.GUID] = o.Name
}

type entry struct {
planName string
offeringGUID string
offeringName string
}
planLookup := make(map[string]entry)
for _, p := range plans {
planLookup[p.GUID] = entry{
planName: p.Name,
offeringGUID: p.ServiceOfferingGUID,
offeringName: offeringLookup[p.ServiceOfferingGUID],
}
func computePlanGUIDLookup(plans []ServicePlan) func(guid string) ServicePlan {
plansLookup := make(map[string]ServicePlan, len(plans))
for _, plan := range plans {
plansLookup[plan.GUID] = plan
}

return func(planGUID string) (string, string, string) {
e := planLookup[planGUID]
return e.planName, e.offeringGUID, e.offeringName
return func(guid string) ServicePlan {
return plansLookup[guid]
}
}

Expand Down Expand Up @@ -144,3 +113,11 @@ func computeSpaceGUIDLookup(spaces []includedSpace, orgs []includedOrganization)
return e.spaceName, e.orgGUID, e.orgName
}
}

func getPlansGUIDs(plans []ServicePlan) []string {
guids := make([]string, 0, len(plans))
for _, plan := range plans {
guids = append(guids, plan.GUID)
}
return guids
}
143 changes: 82 additions & 61 deletions internal/ccapi/service_instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,72 +31,102 @@ var _ = Describe("GetServiceInstances", func() {
fakeServer.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyHeaderKV("Authorization", "fake-token"),
ghttp.VerifyRequest("GET", "/v3/service_instances", ccapi.BuildQueryParams([]string{"test-plan-guid", "another-test-guid"})),
ghttp.VerifyRequest("GET", "/v3/service_instances", ccapi.BuildQueryParams([]string{
"72abfc2f-5473-4fda-b895-a59d47b8f001",
"e55b84e8-b953-4a14-98b2-67bec998a632",
"510da794-1e71-4192-bd39-d974de20b7a4",
})),
ghttp.RespondWith(http.StatusOK, fakeResponse()),
),
)
})

It("returns instances from the given plans", func() {
actualInstances, err := fakeCCAPI.GetServiceInstances([]string{"test-plan-guid", "another-test-guid"})
servicePlanOne := ccapi.ServicePlan{
GUID: "72abfc2f-5473-4fda-b895-a59d47b8f001",
Available: true,
Name: "db-small",
MaintenanceInfoVersion: "1.5.1",
ServiceOfferingGUID: "707cff6a-fc54-471a-9594-442c306fb1d0",
ServiceOfferingName: "fake-service-offering-name-1",
}

servicePlanTwo := ccapi.ServicePlan{
GUID: "e55b84e8-b953-4a14-98b2-67bec998a632",
Available: true,
Name: "postgres-db-f1-micro",
MaintenanceInfoVersion: "",
ServiceOfferingGUID: "8551df49-1fb2-4d12-a009-5307176db52c",
ServiceOfferingName: "fake-service-offering-name-2",
}

servicePlanThree := ccapi.ServicePlan{
GUID: "510da794-1e71-4192-bd39-d974de20b7a4",
Available: true,
Name: "small",
MaintenanceInfoVersion: "",
ServiceOfferingGUID: "ebdddfd4-c95a-4e1a-bdd1-4697ffb57fcd",
ServiceOfferingName: "fake-service-offering-name-3",
}
actualInstances, err := fakeCCAPI.GetServiceInstancesForServicePlans([]ccapi.ServicePlan{servicePlanOne, servicePlanTwo, servicePlanThree})

By("checking the valid service instance is returned")
Expect(err).NotTo(HaveOccurred())

Expect(actualInstances).To(ConsistOf(
ccapi.ServiceInstance{
GUID: "c5518540-7353-4d66-bae7-e07dfed8dd70",
Name: "fake-service-instance-name-1",
UpgradeAvailable: false,
LastOperationType: "create",
LastOperationState: "succeeded",
LastOperationDescription: "Instance provisioning completed",
MaintenanceInfoVersion: "2.10.14-build.3",
ServicePlanGUID: "72abfc2f-5473-4fda-b895-a59d47b8f001",
ServicePlanName: "db-small",
ServiceOfferingGUID: "707cff6a-fc54-471a-9594-442c306fb1d0",
ServiceOfferingName: "fake-service-offering-name-1",
SpaceGUID: "dcf1a90a-47ee-4ba2-a369-255aa00c2de0",
SpaceName: "broker-cf-test",
OrganizationGUID: "69086541-1b9d-449d-b8a4-79029b25e74f",
OrganizationName: "pivotal",
PlanMaintenanceInfoVersion: "", // Not in API response
GUID: "c5518540-7353-4d66-bae7-e07dfed8dd70",
Name: "fake-service-instance-name-1",
UpgradeAvailable: false,
LastOperationType: "create",
LastOperationState: "succeeded",
LastOperationDescription: "Instance provisioning completed",
MaintenanceInfoVersion: "2.10.14-build.3",
ServicePlanGUID: "72abfc2f-5473-4fda-b895-a59d47b8f001",
ServicePlanName: "db-small",
ServiceOfferingGUID: "707cff6a-fc54-471a-9594-442c306fb1d0",
ServiceOfferingName: "fake-service-offering-name-1",
SpaceGUID: "dcf1a90a-47ee-4ba2-a369-255aa00c2de0",
SpaceName: "broker-cf-test",
OrganizationGUID: "69086541-1b9d-449d-b8a4-79029b25e74f",
OrganizationName: "pivotal",
ServicePlanMaintenanceInfoVersion: "1.5.1",
},
ccapi.ServiceInstance{
GUID: "3358305d-7402-48b3-80a7-e0148a38675b",
Name: "fake-service-instance-name-2",
UpgradeAvailable: false,
LastOperationType: "create",
LastOperationState: "succeeded",
LastOperationDescription: "",
MaintenanceInfoVersion: "",
ServicePlanGUID: "e55b84e8-b953-4a14-98b2-67bec998a632",
ServicePlanName: "postgres-db-f1-micro",
ServiceOfferingGUID: "8551df49-1fb2-4d12-a009-5307176db52c",
ServiceOfferingName: "fake-service-offering-name-2",
SpaceGUID: "dcf1a90a-47ee-4ba2-a369-255aa00c2de0",
SpaceName: "broker-cf-test",
OrganizationGUID: "69086541-1b9d-449d-b8a4-79029b25e74f",
OrganizationName: "pivotal",
PlanMaintenanceInfoVersion: "", // Not in API response
GUID: "3358305d-7402-48b3-80a7-e0148a38675b",
Name: "fake-service-instance-name-2",
UpgradeAvailable: false,
LastOperationType: "create",
LastOperationState: "succeeded",
LastOperationDescription: "",
MaintenanceInfoVersion: "",
ServicePlanGUID: "e55b84e8-b953-4a14-98b2-67bec998a632",
ServicePlanName: "postgres-db-f1-micro",
ServiceOfferingGUID: "8551df49-1fb2-4d12-a009-5307176db52c",
ServiceOfferingName: "fake-service-offering-name-2",
SpaceGUID: "dcf1a90a-47ee-4ba2-a369-255aa00c2de0",
SpaceName: "broker-cf-test",
OrganizationGUID: "69086541-1b9d-449d-b8a4-79029b25e74f",
OrganizationName: "pivotal",
ServicePlanMaintenanceInfoVersion: "",
},
ccapi.ServiceInstance{
GUID: "5b528bf8-ac0f-4fed-85d0-0fb5f8588968",
Name: "fake-service-instance-name-3",
UpgradeAvailable: true,
LastOperationType: "update",
LastOperationState: "succeeded",
LastOperationDescription: "update succeeded",
MaintenanceInfoVersion: "1.3.9",
ServicePlanGUID: "510da794-1e71-4192-bd39-d974de20b7a4",
ServicePlanName: "small",
ServiceOfferingGUID: "ebdddfd4-c95a-4e1a-bdd1-4697ffb57fcd",
ServiceOfferingName: "fake-service-offering-name-3",
SpaceGUID: "bbd00d42-8577-11ee-9b75-6feb4799d316",
SpaceName: "broker-csb-test",
OrganizationGUID: "529d3532-87a9-11ee-8a24-d354d25d7923",
OrganizationName: "vmware",
PlanMaintenanceInfoVersion: "", // Not in API response
GUID: "5b528bf8-ac0f-4fed-85d0-0fb5f8588968",
Name: "fake-service-instance-name-3",
UpgradeAvailable: true,
LastOperationType: "update",
LastOperationState: "succeeded",
LastOperationDescription: "update succeeded",
MaintenanceInfoVersion: "1.3.9",
ServicePlanGUID: "510da794-1e71-4192-bd39-d974de20b7a4",
ServicePlanName: "small",
ServiceOfferingGUID: "ebdddfd4-c95a-4e1a-bdd1-4697ffb57fcd",
ServiceOfferingName: "fake-service-offering-name-3",
SpaceGUID: "bbd00d42-8577-11ee-9b75-6feb4799d316",
SpaceName: "broker-csb-test",
OrganizationGUID: "529d3532-87a9-11ee-8a24-d354d25d7923",
OrganizationName: "vmware",
ServicePlanMaintenanceInfoVersion: "",
},
))

Expand All @@ -106,16 +136,7 @@ var _ = Describe("GetServiceInstances", func() {
By("making the appending the plan guids")
Expect(requests[0].Method).To(Equal("GET"))
Expect(requests[0].URL.Path).To(Equal("/v3/service_instances"))
Expect(requests[0].URL.RawQuery).To(Equal("per_page=5000&fields[space]=name,guid,relationships.organization&fields[space.organization]=name,guid&fields[service_plan]=name,guid,relationships.service_offering&fields[service_plan.service_offering]=guid,name&service_plan_guids=test-plan-guid,another-test-guid"))
})
})

When("no plan GUIDs are given", func() {
It("returns an error", func() {
actualInstances, err := fakeCCAPI.GetServiceInstances([]string{})

Expect(err).To(MatchError("no service_plan_guids specified"))
Expect(actualInstances).To(BeNil())
Expect(requests[0].URL.RawQuery).To(Equal("per_page=5000&fields[space]=name,guid,relationships.organization&fields[space.organization]=name,guid&service_plan_guids=72abfc2f-5473-4fda-b895-a59d47b8f001,e55b84e8-b953-4a14-98b2-67bec998a632,510da794-1e71-4192-bd39-d974de20b7a4"))
})
})

Expand All @@ -131,7 +152,7 @@ var _ = Describe("GetServiceInstances", func() {
})

It("returns an error", func() {
_, err := fakeCCAPI.GetServiceInstances([]string{"test-guid"})
_, err := fakeCCAPI.GetServiceInstancesForServicePlans([]ccapi.ServicePlan{{GUID: "test-guid"}})

Expect(err).To(MatchError("error getting service instances: http response: 500"))
})
Expand Down
60 changes: 54 additions & 6 deletions internal/ccapi/service_plans.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,64 @@ import (
)

type ServicePlan struct {
GUID string `json:"guid"`
MaintenanceInfoVersion string `jsonry:"maintenance_info.version"`
GUID string
Available bool
Name string
MaintenanceInfoVersion string
ServiceOfferingGUID string
ServiceOfferingName string
}

func (c CCAPI) GetServicePlans(brokerName string) ([]ServicePlan, error) {

type plan struct {
GUID string `json:"guid"`
Available bool `json:"available"`
Name string `json:"name"`
MaintenanceInfoVersion string `jsonry:"maintenance_info.version"`
IncludedServiceOfferingGUID string `jsonry:"relationships.service_offering.data.guid"`
}

type serviceOffering struct {
GUID string `json:"guid"`
Name string `json:"name"`
}

type includedServiceOfferings struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this type is only every used once, it could be inlined in the receiver struct, but different people will prefer different styles.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, in that way we will avoid polluting the typing system

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll leave it as it is because I find it more readable. Due to that the type is in the function scope, we do not create pollution in the typing system. Thanks for your comment

ServiceOfferings []serviceOffering `json:"service_offerings"`
}

var receiver struct {
Plans []ServicePlan `json:"resources"`
Plans []plan `json:"resources"`
IncludedResources includedServiceOfferings `json:"included"`
}
if err := c.requester.Get(fmt.Sprintf("v3/service_plans?per_page=5000&service_broker_names=%s", brokerName), &receiver); err != nil {
return nil, fmt.Errorf("error getting service plans: %s", err)

if err := c.requester.Get(fmt.Sprintf("v3/service_plans?include=service_offering&per_page=5000&service_broker_names=%s", brokerName), &receiver); err != nil {
return nil, fmt.Errorf("error getting service plans: %w", err)
}
return receiver.Plans, nil

var plans []ServicePlan

serviceOfferingLookup := make(map[string]serviceOffering, len(receiver.IncludedResources.ServiceOfferings))
for _, offering := range receiver.IncludedResources.ServiceOfferings {
serviceOfferingLookup[offering.GUID] = offering
}

for _, p := range receiver.Plans {

sp := ServicePlan{
GUID: p.GUID,
Available: p.Available,
Name: p.Name,
MaintenanceInfoVersion: p.MaintenanceInfoVersion,
}

offering := serviceOfferingLookup[p.IncludedServiceOfferingGUID]
sp.ServiceOfferingGUID = offering.GUID
sp.ServiceOfferingName = offering.Name

plans = append(plans, sp)
}

return plans, nil
}
Loading
Loading