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) + } }