From 11d7ce622f7b2463ee0e5222807581e2e0dc4eb1 Mon Sep 17 00:00:00 2001 From: Matt Sicker Date: Fri, 7 Jan 2022 17:27:07 -0600 Subject: [PATCH] feat(web): Expose experimental account storage API (#1494) * feat(web): Expose experimental account storage API This adds some of the REST APIs introduced in the experimental account storage API in Clouddriver to Gate. Initially, these APIs are only available for admins. * Combine account and credentials endpoints * Add docs on AccountDefinition * Add alpha annotations --- .../services/internal/ClouddriverService.java | 52 ++++++++++++++++ .../controllers/CredentialsController.groovy | 62 +++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/ClouddriverService.java b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/ClouddriverService.java index 93f84a014f..de23913437 100644 --- a/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/ClouddriverService.java +++ b/gate-core/src/main/java/com/netflix/spinnaker/gate/services/internal/ClouddriverService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; import com.netflix.spinnaker.kork.plugins.SpinnakerPluginDescriptor; import java.util.ArrayList; import java.util.Collection; @@ -12,6 +13,7 @@ import java.util.Map; import retrofit.client.Response; import retrofit.http.Body; +import retrofit.http.DELETE; import retrofit.http.GET; import retrofit.http.Headers; import retrofit.http.POST; @@ -31,6 +33,21 @@ public interface ClouddriverService { @GET("/credentials/{account}") AccountDetails getAccount(@Path("account") String account); + @GET("/credentials/type/{type}") + List getAccountDefinitionsByType( + @Path("type") String type, + @Query("limit") Integer limit, + @Query("startingAccountName") String startingAccountName); + + @POST("/credentials") + AccountDefinition createAccountDefinition(@Body AccountDefinition accountDefinition); + + @PUT("/credentials") + AccountDefinition updateAccountDefinition(@Body AccountDefinition accountDefinition); + + @DELETE("/credentials/{account}") + void deleteAccountDefinition(@Path("account") String account); + @GET("/task/{taskDetailsId}") Map getTaskDetails(@Path("taskDetailsId") String taskDetailsId); @@ -493,4 +510,39 @@ public void setCloudProvider(String cloudProvider) { private String cloudProvider; private final Map details = new HashMap(); } + + /** + * Wrapper type for Clouddriver account definitions. Clouddriver account definitions implement + * {@code CredentialsDefinition}, and its type discriminator is present in a property named + * {@code @type}. An instance of an account definition may have fairly different properties than + * its corresponding {@code AccountCredentials} instance. Account definitions must store all the + * relevant properties unchanged while {@link Account} and {@link AccountDetails} may summarize + * and remove data returned from their corresponding APIs. Account definitions must be transformed + * by a {@code CredentialsParser} before their corresponding credentials may be used by + * Clouddriver. + */ + class AccountDefinition { + private final Map details = new HashMap<>(); + private String type; + + @JsonAnyGetter + public Map details() { + return details; + } + + @JsonAnySetter + public void set(String name, Object value) { + details.put(name, value); + } + + @JsonProperty("@type") + public String getType() { + return type; + } + + @JsonProperty("@type") + public void setType(String type) { + this.type = type; + } + } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/CredentialsController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/CredentialsController.groovy index 600cbf7515..cf36caa9f5 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/CredentialsController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/CredentialsController.groovy @@ -21,12 +21,22 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spinnaker.gate.security.AllowedAccountsSupport import com.netflix.spinnaker.gate.security.SpinnakerUser import com.netflix.spinnaker.gate.services.AccountLookupService +import com.netflix.spinnaker.gate.services.internal.ClouddriverService import com.netflix.spinnaker.gate.services.internal.ClouddriverService.Account import com.netflix.spinnaker.gate.services.internal.ClouddriverService.AccountDetails +import com.netflix.spinnaker.kork.annotations.Alpha import com.netflix.spinnaker.security.User import io.swagger.annotations.ApiOperation +import io.swagger.annotations.ApiParam import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.access.prepost.PostFilter +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod @@ -43,6 +53,9 @@ class CredentialsController { @Autowired AllowedAccountsSupport allowedAccountsSupport + @Autowired + ClouddriverService clouddriverService + @Autowired ObjectMapper objectMapper @@ -78,4 +91,53 @@ class CredentialsController { @RequestHeader(value = "X-RateLimit-App", required = false) String sourceApp) { return getAccountDetailsWithAuthorizedFlag(user).find { it.name == account } } + + @GetMapping('/type/{accountType}') + @ApiOperation('Looks up account definitions by type.') + @PostFilter("hasPermission(filterObject.name, 'ACCOUNT', 'WRITE')") + @Alpha + List getAccountsByType( + @ApiParam(value = 'Value of the "@type" key for accounts to search for.', example = 'kubernetes') + @PathVariable String accountType, + @ApiParam('Maximum number of entries to return in results. Used for pagination.') + @RequestParam OptionalInt limit, + @ApiParam('Account name to start account definition listing from. Used for pagination.') + @RequestParam Optional startingAccountName + ) { + clouddriverService.getAccountDefinitionsByType(accountType, limit.isPresent() ? limit.getAsInt() : null, startingAccountName.orElse(null)) + } + + @PostMapping + @ApiOperation('Creates a new account definition.') + @PreAuthorize('isAuthenticated()') + @Alpha + ClouddriverService.AccountDefinition createAccount( + @ApiParam('Account definition body including a discriminator field named "@type" with the account type.') + @RequestBody ClouddriverService.AccountDefinition accountDefinition + ) { + clouddriverService.createAccountDefinition(accountDefinition) + } + + @PutMapping + @ApiOperation('Updates an existing account definition.') + @PreAuthorize("hasPermission(#definition.name, 'ACCOUNT', 'WRITE')") + @Alpha + ClouddriverService.AccountDefinition updateAccount( + @ApiParam('Account definition body including a discriminator field named "@type" with the account type.') + @RequestBody ClouddriverService.AccountDefinition accountDefinition + ) { + clouddriverService.updateAccountDefinition(accountDefinition) + } + + @DeleteMapping('/{accountName}') + @ApiOperation(value = 'Deletes an account definition by name.', + notes = 'Deleted accounts can be restored via the update API. Previously deleted accounts cannot be "created" again to avoid conflicts with existing pipelines.') + @PreAuthorize("hasPermission(#definition.name, 'ACCOUNT', 'WRITE')") + @Alpha + void deleteAccount( + @ApiParam('Name of account definition to delete.') + @PathVariable String accountName + ) { + clouddriverService.deleteAccountDefinition(accountName) + } }