Add override for aks 2018-03-31 and autorest
This commit is contained in:
parent
3f991e1dc5
commit
6f6e21d689
|
|
@ -42,11 +42,11 @@ func NewContainerServicesClientWithBaseURI(baseURI string, subscriptionID string
|
||||||
|
|
||||||
// CreateOrUpdate creates or updates a container service with the specified configuration of orchestrator, masters, and
|
// CreateOrUpdate creates or updates a container service with the specified configuration of orchestrator, masters, and
|
||||||
// agents.
|
// agents.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group. containerServiceName is the name of the container service in
|
// resourceGroupName - the name of the resource group.
|
||||||
// the specified subscription and resource group. parameters is parameters supplied to the Create or Update a Container
|
// containerServiceName - the name of the container service in the specified subscription and resource group.
|
||||||
// Service operation.
|
// parameters - parameters supplied to the Create or Update a Container Service operation.
|
||||||
func (client ContainerServicesClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, containerServiceName string, parameters ContainerService) (result ContainerServicesCreateOrUpdateFuture, err error) {
|
func (client ContainerServicesClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, containerServiceName string, parameters ContainerService) (result ContainerServicesCreateOrUpdateFutureType, err error) {
|
||||||
if err := validation.Validate([]validation.Validation{
|
if err := validation.Validate([]validation.Validation{
|
||||||
{TargetValue: parameters,
|
{TargetValue: parameters,
|
||||||
Constraints: []validation.Constraint{{Target: "parameters.Properties", Name: validation.Null, Rule: false,
|
Constraints: []validation.Constraint{{Target: "parameters.Properties", Name: validation.Null, Rule: false,
|
||||||
|
|
@ -65,8 +65,7 @@ func (client ContainerServicesClient) CreateOrUpdate(ctx context.Context, resour
|
||||||
{Target: "parameters.Properties.WindowsProfile", Name: validation.Null, Rule: false,
|
{Target: "parameters.Properties.WindowsProfile", Name: validation.Null, Rule: false,
|
||||||
Chain: []validation.Constraint{{Target: "parameters.Properties.WindowsProfile.AdminUsername", Name: validation.Null, Rule: true,
|
Chain: []validation.Constraint{{Target: "parameters.Properties.WindowsProfile.AdminUsername", Name: validation.Null, Rule: true,
|
||||||
Chain: []validation.Constraint{{Target: "parameters.Properties.WindowsProfile.AdminUsername", Name: validation.Pattern, Rule: `^[a-zA-Z0-9]+([._]?[a-zA-Z0-9]+)*$`, Chain: nil}}},
|
Chain: []validation.Constraint{{Target: "parameters.Properties.WindowsProfile.AdminUsername", Name: validation.Pattern, Rule: `^[a-zA-Z0-9]+([._]?[a-zA-Z0-9]+)*$`, Chain: nil}}},
|
||||||
{Target: "parameters.Properties.WindowsProfile.AdminPassword", Name: validation.Null, Rule: true,
|
{Target: "parameters.Properties.WindowsProfile.AdminPassword", Name: validation.Null, Rule: true, Chain: nil},
|
||||||
Chain: []validation.Constraint{{Target: "parameters.Properties.WindowsProfile.AdminPassword", Name: validation.Pattern, Rule: `^(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%\^&\*\(\)])[a-zA-Z\d!@#$%\^&\*\(\)]{12,123}$`, Chain: nil}}},
|
|
||||||
}},
|
}},
|
||||||
{Target: "parameters.Properties.LinuxProfile", Name: validation.Null, Rule: true,
|
{Target: "parameters.Properties.LinuxProfile", Name: validation.Null, Rule: true,
|
||||||
Chain: []validation.Constraint{{Target: "parameters.Properties.LinuxProfile.AdminUsername", Name: validation.Null, Rule: true,
|
Chain: []validation.Constraint{{Target: "parameters.Properties.LinuxProfile.AdminUsername", Name: validation.Null, Rule: true,
|
||||||
|
|
@ -79,7 +78,7 @@ func (client ContainerServicesClient) CreateOrUpdate(ctx context.Context, resour
|
||||||
Chain: []validation.Constraint{{Target: "parameters.Properties.DiagnosticsProfile.VMDiagnostics.Enabled", Name: validation.Null, Rule: true, Chain: nil}}},
|
Chain: []validation.Constraint{{Target: "parameters.Properties.DiagnosticsProfile.VMDiagnostics.Enabled", Name: validation.Null, Rule: true, Chain: nil}}},
|
||||||
}},
|
}},
|
||||||
}}}}}); err != nil {
|
}}}}}); err != nil {
|
||||||
return result, validation.NewErrorWithValidationError(err, "containerservice.ContainerServicesClient", "CreateOrUpdate")
|
return result, validation.NewError("containerservice.ContainerServicesClient", "CreateOrUpdate", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := client.CreateOrUpdatePreparer(ctx, resourceGroupName, containerServiceName, parameters)
|
req, err := client.CreateOrUpdatePreparer(ctx, resourceGroupName, containerServiceName, parameters)
|
||||||
|
|
@ -111,7 +110,7 @@ func (client ContainerServicesClient) CreateOrUpdatePreparer(ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
preparer := autorest.CreatePreparer(
|
||||||
autorest.AsJSON(),
|
autorest.AsContentType("application/json; charset=utf-8"),
|
||||||
autorest.AsPut(),
|
autorest.AsPut(),
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
autorest.WithBaseURL(client.BaseURI),
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContainerService/containerServices/{containerServiceName}", pathParameters),
|
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContainerService/containerServices/{containerServiceName}", pathParameters),
|
||||||
|
|
@ -122,16 +121,18 @@ func (client ContainerServicesClient) CreateOrUpdatePreparer(ctx context.Context
|
||||||
|
|
||||||
// CreateOrUpdateSender sends the CreateOrUpdate request. The method will close the
|
// CreateOrUpdateSender sends the CreateOrUpdate request. The method will close the
|
||||||
// http.Response Body if it receives an error.
|
// http.Response Body if it receives an error.
|
||||||
func (client ContainerServicesClient) CreateOrUpdateSender(req *http.Request) (future ContainerServicesCreateOrUpdateFuture, err error) {
|
func (client ContainerServicesClient) CreateOrUpdateSender(req *http.Request) (future ContainerServicesCreateOrUpdateFutureType, err error) {
|
||||||
sender := autorest.DecorateSender(client, azure.DoRetryWithRegistration(client.Client))
|
var resp *http.Response
|
||||||
future.Future = azure.NewFuture(req)
|
resp, err = autorest.SendWithSender(client, req,
|
||||||
future.req = req
|
azure.DoRetryWithRegistration(client.Client))
|
||||||
_, err = future.Done(sender)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = autorest.Respond(future.Response(),
|
err = autorest.Respond(resp, azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated, http.StatusAccepted))
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated, http.StatusAccepted))
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
future.Future, err = azure.NewFutureFromResponse(resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,10 +153,10 @@ func (client ContainerServicesClient) CreateOrUpdateResponder(resp *http.Respons
|
||||||
// not delete other resources created as part of creating a container service, including storage accounts, VMs, and
|
// not delete other resources created as part of creating a container service, including storage accounts, VMs, and
|
||||||
// availability sets. All the other resources created with the container service are part of the same resource group
|
// availability sets. All the other resources created with the container service are part of the same resource group
|
||||||
// and can be deleted individually.
|
// and can be deleted individually.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group. containerServiceName is the name of the container service in
|
// resourceGroupName - the name of the resource group.
|
||||||
// the specified subscription and resource group.
|
// containerServiceName - the name of the container service in the specified subscription and resource group.
|
||||||
func (client ContainerServicesClient) Delete(ctx context.Context, resourceGroupName string, containerServiceName string) (result ContainerServicesDeleteFuture, err error) {
|
func (client ContainerServicesClient) Delete(ctx context.Context, resourceGroupName string, containerServiceName string) (result ContainerServicesDeleteFutureType, err error) {
|
||||||
req, err := client.DeletePreparer(ctx, resourceGroupName, containerServiceName)
|
req, err := client.DeletePreparer(ctx, resourceGroupName, containerServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = autorest.NewErrorWithError(err, "containerservice.ContainerServicesClient", "Delete", nil, "Failure preparing request")
|
err = autorest.NewErrorWithError(err, "containerservice.ContainerServicesClient", "Delete", nil, "Failure preparing request")
|
||||||
|
|
@ -194,16 +195,18 @@ func (client ContainerServicesClient) DeletePreparer(ctx context.Context, resour
|
||||||
|
|
||||||
// DeleteSender sends the Delete request. The method will close the
|
// DeleteSender sends the Delete request. The method will close the
|
||||||
// http.Response Body if it receives an error.
|
// http.Response Body if it receives an error.
|
||||||
func (client ContainerServicesClient) DeleteSender(req *http.Request) (future ContainerServicesDeleteFuture, err error) {
|
func (client ContainerServicesClient) DeleteSender(req *http.Request) (future ContainerServicesDeleteFutureType, err error) {
|
||||||
sender := autorest.DecorateSender(client, azure.DoRetryWithRegistration(client.Client))
|
var resp *http.Response
|
||||||
future.Future = azure.NewFuture(req)
|
resp, err = autorest.SendWithSender(client, req,
|
||||||
future.req = req
|
azure.DoRetryWithRegistration(client.Client))
|
||||||
_, err = future.Done(sender)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = autorest.Respond(future.Response(),
|
err = autorest.Respond(resp, azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted, http.StatusNoContent))
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted, http.StatusNoContent))
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
future.Future, err = azure.NewFutureFromResponse(resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,9 +225,9 @@ func (client ContainerServicesClient) DeleteResponder(resp *http.Response) (resu
|
||||||
// Get gets the properties of the specified container service in the specified subscription and resource group. The
|
// Get gets the properties of the specified container service in the specified subscription and resource group. The
|
||||||
// operation returns the properties including state, orchestrator, number of masters and agents, and FQDNs of masters
|
// operation returns the properties including state, orchestrator, number of masters and agents, and FQDNs of masters
|
||||||
// and agents.
|
// and agents.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group. containerServiceName is the name of the container service in
|
// resourceGroupName - the name of the resource group.
|
||||||
// the specified subscription and resource group.
|
// containerServiceName - the name of the container service in the specified subscription and resource group.
|
||||||
func (client ContainerServicesClient) Get(ctx context.Context, resourceGroupName string, containerServiceName string) (result ContainerService, err error) {
|
func (client ContainerServicesClient) Get(ctx context.Context, resourceGroupName string, containerServiceName string) (result ContainerService, err error) {
|
||||||
req, err := client.GetPreparer(ctx, resourceGroupName, containerServiceName)
|
req, err := client.GetPreparer(ctx, resourceGroupName, containerServiceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -382,8 +385,8 @@ func (client ContainerServicesClient) ListComplete(ctx context.Context) (result
|
||||||
// ListByResourceGroup gets a list of container services in the specified subscription and resource group. The
|
// ListByResourceGroup gets a list of container services in the specified subscription and resource group. The
|
||||||
// operation returns properties of each container service including state, orchestrator, number of masters and agents,
|
// operation returns properties of each container service including state, orchestrator, number of masters and agents,
|
||||||
// and FQDNs of masters and agents.
|
// and FQDNs of masters and agents.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group.
|
// resourceGroupName - the name of the resource group.
|
||||||
func (client ContainerServicesClient) ListByResourceGroup(ctx context.Context, resourceGroupName string) (result ListResultPage, err error) {
|
func (client ContainerServicesClient) ListByResourceGroup(ctx context.Context, resourceGroupName string) (result ListResultPage, err error) {
|
||||||
result.fn = client.listByResourceGroupNextResults
|
result.fn = client.listByResourceGroupNextResults
|
||||||
req, err := client.ListByResourceGroupPreparer(ctx, resourceGroupName)
|
req, err := client.ListByResourceGroupPreparer(ctx, resourceGroupName)
|
||||||
|
|
@ -476,9 +479,9 @@ func (client ContainerServicesClient) ListByResourceGroupComplete(ctx context.Co
|
||||||
|
|
||||||
// ListOrchestrators gets a list of supported orchestrators in the specified subscription. The operation returns
|
// ListOrchestrators gets a list of supported orchestrators in the specified subscription. The operation returns
|
||||||
// properties of each orchestrator including version and available upgrades.
|
// properties of each orchestrator including version and available upgrades.
|
||||||
//
|
// Parameters:
|
||||||
// location is the name of a supported Azure region. resourceType is resource type for which the list of orchestrators
|
// location - the name of a supported Azure region.
|
||||||
// needs to be returned
|
// resourceType - resource type for which the list of orchestrators needs to be returned
|
||||||
func (client ContainerServicesClient) ListOrchestrators(ctx context.Context, location string, resourceType string) (result OrchestratorVersionProfileListResult, err error) {
|
func (client ContainerServicesClient) ListOrchestrators(ctx context.Context, location string, resourceType string) (result OrchestratorVersionProfileListResult, err error) {
|
||||||
req, err := client.ListOrchestratorsPreparer(ctx, location, resourceType)
|
req, err := client.ListOrchestratorsPreparer(ctx, location, resourceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -42,9 +42,10 @@ func NewManagedClustersClientWithBaseURI(baseURI string, subscriptionID string)
|
||||||
|
|
||||||
// CreateOrUpdate creates or updates a managed cluster with the specified configuration for agents and Kubernetes
|
// CreateOrUpdate creates or updates a managed cluster with the specified configuration for agents and Kubernetes
|
||||||
// version.
|
// version.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group. resourceName is the name of the managed cluster resource.
|
// resourceGroupName - the name of the resource group.
|
||||||
// parameters is parameters supplied to the Create or Update a Managed Cluster operation.
|
// resourceName - the name of the managed cluster resource.
|
||||||
|
// parameters - parameters supplied to the Create or Update a Managed Cluster operation.
|
||||||
func (client ManagedClustersClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, resourceName string, parameters ManagedCluster) (result ManagedClustersCreateOrUpdateFuture, err error) {
|
func (client ManagedClustersClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, resourceName string, parameters ManagedCluster) (result ManagedClustersCreateOrUpdateFuture, err error) {
|
||||||
if err := validation.Validate([]validation.Validation{
|
if err := validation.Validate([]validation.Validation{
|
||||||
{TargetValue: parameters,
|
{TargetValue: parameters,
|
||||||
|
|
@ -62,8 +63,23 @@ func (client ManagedClustersClient) CreateOrUpdate(ctx context.Context, resource
|
||||||
{Target: "parameters.ManagedClusterProperties.ServicePrincipalProfile.KeyVaultSecretRef.SecretName", Name: validation.Null, Rule: true, Chain: nil},
|
{Target: "parameters.ManagedClusterProperties.ServicePrincipalProfile.KeyVaultSecretRef.SecretName", Name: validation.Null, Rule: true, Chain: nil},
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
|
{Target: "parameters.ManagedClusterProperties.NetworkProfile", Name: validation.Null, Rule: false,
|
||||||
|
Chain: []validation.Constraint{{Target: "parameters.ManagedClusterProperties.NetworkProfile.PodCidr", Name: validation.Null, Rule: false,
|
||||||
|
Chain: []validation.Constraint{{Target: "parameters.ManagedClusterProperties.NetworkProfile.PodCidr", Name: validation.Pattern, Rule: `^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$`, Chain: nil}}},
|
||||||
|
{Target: "parameters.ManagedClusterProperties.NetworkProfile.ServiceCidr", Name: validation.Null, Rule: false,
|
||||||
|
Chain: []validation.Constraint{{Target: "parameters.ManagedClusterProperties.NetworkProfile.ServiceCidr", Name: validation.Pattern, Rule: `^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$`, Chain: nil}}},
|
||||||
|
{Target: "parameters.ManagedClusterProperties.NetworkProfile.DNSServiceIP", Name: validation.Null, Rule: false,
|
||||||
|
Chain: []validation.Constraint{{Target: "parameters.ManagedClusterProperties.NetworkProfile.DNSServiceIP", Name: validation.Pattern, Rule: `^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`, Chain: nil}}},
|
||||||
|
{Target: "parameters.ManagedClusterProperties.NetworkProfile.DockerBridgeCidr", Name: validation.Null, Rule: false,
|
||||||
|
Chain: []validation.Constraint{{Target: "parameters.ManagedClusterProperties.NetworkProfile.DockerBridgeCidr", Name: validation.Pattern, Rule: `^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$`, Chain: nil}}},
|
||||||
|
}},
|
||||||
|
{Target: "parameters.ManagedClusterProperties.AadProfile", Name: validation.Null, Rule: false,
|
||||||
|
Chain: []validation.Constraint{{Target: "parameters.ManagedClusterProperties.AadProfile.ClientAppID", Name: validation.Null, Rule: true, Chain: nil},
|
||||||
|
{Target: "parameters.ManagedClusterProperties.AadProfile.ServerAppID", Name: validation.Null, Rule: true, Chain: nil},
|
||||||
|
{Target: "parameters.ManagedClusterProperties.AadProfile.ServerAppSecret", Name: validation.Null, Rule: true, Chain: nil},
|
||||||
|
}},
|
||||||
}}}}}); err != nil {
|
}}}}}); err != nil {
|
||||||
return result, validation.NewErrorWithValidationError(err, "containerservice.ManagedClustersClient", "CreateOrUpdate")
|
return result, validation.NewError("containerservice.ManagedClustersClient", "CreateOrUpdate", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := client.CreateOrUpdatePreparer(ctx, resourceGroupName, resourceName, parameters)
|
req, err := client.CreateOrUpdatePreparer(ctx, resourceGroupName, resourceName, parameters)
|
||||||
|
|
@ -89,13 +105,13 @@ func (client ManagedClustersClient) CreateOrUpdatePreparer(ctx context.Context,
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
const APIVersion = "2017-08-31"
|
const APIVersion = "2018-03-31"
|
||||||
queryParameters := map[string]interface{}{
|
queryParameters := map[string]interface{}{
|
||||||
"api-version": APIVersion,
|
"api-version": APIVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
preparer := autorest.CreatePreparer(
|
||||||
autorest.AsJSON(),
|
autorest.AsContentType("application/json; charset=utf-8"),
|
||||||
autorest.AsPut(),
|
autorest.AsPut(),
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
autorest.WithBaseURL(client.BaseURI),
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContainerService/managedClusters/{resourceName}", pathParameters),
|
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContainerService/managedClusters/{resourceName}", pathParameters),
|
||||||
|
|
@ -107,15 +123,17 @@ func (client ManagedClustersClient) CreateOrUpdatePreparer(ctx context.Context,
|
||||||
// CreateOrUpdateSender sends the CreateOrUpdate request. The method will close the
|
// CreateOrUpdateSender sends the CreateOrUpdate request. The method will close the
|
||||||
// http.Response Body if it receives an error.
|
// http.Response Body if it receives an error.
|
||||||
func (client ManagedClustersClient) CreateOrUpdateSender(req *http.Request) (future ManagedClustersCreateOrUpdateFuture, err error) {
|
func (client ManagedClustersClient) CreateOrUpdateSender(req *http.Request) (future ManagedClustersCreateOrUpdateFuture, err error) {
|
||||||
sender := autorest.DecorateSender(client, azure.DoRetryWithRegistration(client.Client))
|
var resp *http.Response
|
||||||
future.Future = azure.NewFuture(req)
|
resp, err = autorest.SendWithSender(client, req,
|
||||||
future.req = req
|
azure.DoRetryWithRegistration(client.Client))
|
||||||
_, err = future.Done(sender)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = autorest.Respond(future.Response(),
|
err = autorest.Respond(resp, azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated))
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusCreated))
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
future.Future, err = azure.NewFutureFromResponse(resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,8 +151,9 @@ func (client ManagedClustersClient) CreateOrUpdateResponder(resp *http.Response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes the managed cluster with a specified resource group and name.
|
// Delete deletes the managed cluster with a specified resource group and name.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group. resourceName is the name of the managed cluster resource.
|
// resourceGroupName - the name of the resource group.
|
||||||
|
// resourceName - the name of the managed cluster resource.
|
||||||
func (client ManagedClustersClient) Delete(ctx context.Context, resourceGroupName string, resourceName string) (result ManagedClustersDeleteFuture, err error) {
|
func (client ManagedClustersClient) Delete(ctx context.Context, resourceGroupName string, resourceName string) (result ManagedClustersDeleteFuture, err error) {
|
||||||
req, err := client.DeletePreparer(ctx, resourceGroupName, resourceName)
|
req, err := client.DeletePreparer(ctx, resourceGroupName, resourceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -159,7 +178,7 @@ func (client ManagedClustersClient) DeletePreparer(ctx context.Context, resource
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
const APIVersion = "2017-08-31"
|
const APIVersion = "2018-03-31"
|
||||||
queryParameters := map[string]interface{}{
|
queryParameters := map[string]interface{}{
|
||||||
"api-version": APIVersion,
|
"api-version": APIVersion,
|
||||||
}
|
}
|
||||||
|
|
@ -175,15 +194,17 @@ func (client ManagedClustersClient) DeletePreparer(ctx context.Context, resource
|
||||||
// DeleteSender sends the Delete request. The method will close the
|
// DeleteSender sends the Delete request. The method will close the
|
||||||
// http.Response Body if it receives an error.
|
// http.Response Body if it receives an error.
|
||||||
func (client ManagedClustersClient) DeleteSender(req *http.Request) (future ManagedClustersDeleteFuture, err error) {
|
func (client ManagedClustersClient) DeleteSender(req *http.Request) (future ManagedClustersDeleteFuture, err error) {
|
||||||
sender := autorest.DecorateSender(client, azure.DoRetryWithRegistration(client.Client))
|
var resp *http.Response
|
||||||
future.Future = azure.NewFuture(req)
|
resp, err = autorest.SendWithSender(client, req,
|
||||||
future.req = req
|
azure.DoRetryWithRegistration(client.Client))
|
||||||
_, err = future.Done(sender)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = autorest.Respond(future.Response(),
|
err = autorest.Respond(resp, azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted, http.StatusNoContent))
|
||||||
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted, http.StatusNoContent))
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
future.Future, err = azure.NewFutureFromResponse(resp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,8 +221,9 @@ func (client ManagedClustersClient) DeleteResponder(resp *http.Response) (result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get gets the details of the managed cluster with a specified resource group and name.
|
// Get gets the details of the managed cluster with a specified resource group and name.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group. resourceName is the name of the managed cluster resource.
|
// resourceGroupName - the name of the resource group.
|
||||||
|
// resourceName - the name of the managed cluster resource.
|
||||||
func (client ManagedClustersClient) Get(ctx context.Context, resourceGroupName string, resourceName string) (result ManagedCluster, err error) {
|
func (client ManagedClustersClient) Get(ctx context.Context, resourceGroupName string, resourceName string) (result ManagedCluster, err error) {
|
||||||
req, err := client.GetPreparer(ctx, resourceGroupName, resourceName)
|
req, err := client.GetPreparer(ctx, resourceGroupName, resourceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -232,7 +254,7 @@ func (client ManagedClustersClient) GetPreparer(ctx context.Context, resourceGro
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
const APIVersion = "2017-08-31"
|
const APIVersion = "2018-03-31"
|
||||||
queryParameters := map[string]interface{}{
|
queryParameters := map[string]interface{}{
|
||||||
"api-version": APIVersion,
|
"api-version": APIVersion,
|
||||||
}
|
}
|
||||||
|
|
@ -265,35 +287,36 @@ func (client ManagedClustersClient) GetResponder(resp *http.Response) (result Ma
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccessProfiles gets the accessProfile for the specified role name of the managed cluster with a specified
|
// GetAccessProfile gets the accessProfile for the specified role name of the managed cluster with a specified resource
|
||||||
// resource group and name.
|
// group and name.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group. resourceName is the name of the managed cluster resource.
|
// resourceGroupName - the name of the resource group.
|
||||||
// roleName is the name of the role for managed cluster accessProfile resource.
|
// resourceName - the name of the managed cluster resource.
|
||||||
func (client ManagedClustersClient) GetAccessProfiles(ctx context.Context, resourceGroupName string, resourceName string, roleName string) (result ManagedClusterAccessProfile, err error) {
|
// roleName - the name of the role for managed cluster accessProfile resource.
|
||||||
req, err := client.GetAccessProfilesPreparer(ctx, resourceGroupName, resourceName, roleName)
|
func (client ManagedClustersClient) GetAccessProfile(ctx context.Context, resourceGroupName string, resourceName string, roleName string) (result ManagedClusterAccessProfile, err error) {
|
||||||
|
req, err := client.GetAccessProfilePreparer(ctx, resourceGroupName, resourceName, roleName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = autorest.NewErrorWithError(err, "containerservice.ManagedClustersClient", "GetAccessProfiles", nil, "Failure preparing request")
|
err = autorest.NewErrorWithError(err, "containerservice.ManagedClustersClient", "GetAccessProfile", nil, "Failure preparing request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.GetAccessProfilesSender(req)
|
resp, err := client.GetAccessProfileSender(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Response = autorest.Response{Response: resp}
|
result.Response = autorest.Response{Response: resp}
|
||||||
err = autorest.NewErrorWithError(err, "containerservice.ManagedClustersClient", "GetAccessProfiles", resp, "Failure sending request")
|
err = autorest.NewErrorWithError(err, "containerservice.ManagedClustersClient", "GetAccessProfile", resp, "Failure sending request")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err = client.GetAccessProfilesResponder(resp)
|
result, err = client.GetAccessProfileResponder(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = autorest.NewErrorWithError(err, "containerservice.ManagedClustersClient", "GetAccessProfiles", resp, "Failure responding to request")
|
err = autorest.NewErrorWithError(err, "containerservice.ManagedClustersClient", "GetAccessProfile", resp, "Failure responding to request")
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccessProfilesPreparer prepares the GetAccessProfiles request.
|
// GetAccessProfilePreparer prepares the GetAccessProfile request.
|
||||||
func (client ManagedClustersClient) GetAccessProfilesPreparer(ctx context.Context, resourceGroupName string, resourceName string, roleName string) (*http.Request, error) {
|
func (client ManagedClustersClient) GetAccessProfilePreparer(ctx context.Context, resourceGroupName string, resourceName string, roleName string) (*http.Request, error) {
|
||||||
pathParameters := map[string]interface{}{
|
pathParameters := map[string]interface{}{
|
||||||
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
"resourceGroupName": autorest.Encode("path", resourceGroupName),
|
||||||
"resourceName": autorest.Encode("path", resourceName),
|
"resourceName": autorest.Encode("path", resourceName),
|
||||||
|
|
@ -301,29 +324,29 @@ func (client ManagedClustersClient) GetAccessProfilesPreparer(ctx context.Contex
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
const APIVersion = "2017-08-31"
|
const APIVersion = "2018-03-31"
|
||||||
queryParameters := map[string]interface{}{
|
queryParameters := map[string]interface{}{
|
||||||
"api-version": APIVersion,
|
"api-version": APIVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
preparer := autorest.CreatePreparer(
|
preparer := autorest.CreatePreparer(
|
||||||
autorest.AsGet(),
|
autorest.AsPost(),
|
||||||
autorest.WithBaseURL(client.BaseURI),
|
autorest.WithBaseURL(client.BaseURI),
|
||||||
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContainerService/managedClusters/{resourceName}/accessProfiles/{roleName}", pathParameters),
|
autorest.WithPathParameters("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContainerService/managedClusters/{resourceName}/accessProfiles/{roleName}/listCredential", pathParameters),
|
||||||
autorest.WithQueryParameters(queryParameters))
|
autorest.WithQueryParameters(queryParameters))
|
||||||
return preparer.Prepare((&http.Request{}).WithContext(ctx))
|
return preparer.Prepare((&http.Request{}).WithContext(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccessProfilesSender sends the GetAccessProfiles request. The method will close the
|
// GetAccessProfileSender sends the GetAccessProfile request. The method will close the
|
||||||
// http.Response Body if it receives an error.
|
// http.Response Body if it receives an error.
|
||||||
func (client ManagedClustersClient) GetAccessProfilesSender(req *http.Request) (*http.Response, error) {
|
func (client ManagedClustersClient) GetAccessProfileSender(req *http.Request) (*http.Response, error) {
|
||||||
return autorest.SendWithSender(client, req,
|
return autorest.SendWithSender(client, req,
|
||||||
azure.DoRetryWithRegistration(client.Client))
|
azure.DoRetryWithRegistration(client.Client))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAccessProfilesResponder handles the response to the GetAccessProfiles request. The method always
|
// GetAccessProfileResponder handles the response to the GetAccessProfile request. The method always
|
||||||
// closes the http.Response Body.
|
// closes the http.Response Body.
|
||||||
func (client ManagedClustersClient) GetAccessProfilesResponder(resp *http.Response) (result ManagedClusterAccessProfile, err error) {
|
func (client ManagedClustersClient) GetAccessProfileResponder(resp *http.Response) (result ManagedClusterAccessProfile, err error) {
|
||||||
err = autorest.Respond(
|
err = autorest.Respond(
|
||||||
resp,
|
resp,
|
||||||
client.ByInspecting(),
|
client.ByInspecting(),
|
||||||
|
|
@ -336,8 +359,9 @@ func (client ManagedClustersClient) GetAccessProfilesResponder(resp *http.Respon
|
||||||
|
|
||||||
// GetUpgradeProfile gets the details of the upgrade profile for a managed cluster with a specified resource group and
|
// GetUpgradeProfile gets the details of the upgrade profile for a managed cluster with a specified resource group and
|
||||||
// name.
|
// name.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group. resourceName is the name of the managed cluster resource.
|
// resourceGroupName - the name of the resource group.
|
||||||
|
// resourceName - the name of the managed cluster resource.
|
||||||
func (client ManagedClustersClient) GetUpgradeProfile(ctx context.Context, resourceGroupName string, resourceName string) (result ManagedClusterUpgradeProfile, err error) {
|
func (client ManagedClustersClient) GetUpgradeProfile(ctx context.Context, resourceGroupName string, resourceName string) (result ManagedClusterUpgradeProfile, err error) {
|
||||||
req, err := client.GetUpgradeProfilePreparer(ctx, resourceGroupName, resourceName)
|
req, err := client.GetUpgradeProfilePreparer(ctx, resourceGroupName, resourceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -368,7 +392,7 @@ func (client ManagedClustersClient) GetUpgradeProfilePreparer(ctx context.Contex
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
const APIVersion = "2017-08-31"
|
const APIVersion = "2018-03-31"
|
||||||
queryParameters := map[string]interface{}{
|
queryParameters := map[string]interface{}{
|
||||||
"api-version": APIVersion,
|
"api-version": APIVersion,
|
||||||
}
|
}
|
||||||
|
|
@ -432,7 +456,7 @@ func (client ManagedClustersClient) ListPreparer(ctx context.Context) (*http.Req
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
const APIVersion = "2017-08-31"
|
const APIVersion = "2018-03-31"
|
||||||
queryParameters := map[string]interface{}{
|
queryParameters := map[string]interface{}{
|
||||||
"api-version": APIVersion,
|
"api-version": APIVersion,
|
||||||
}
|
}
|
||||||
|
|
@ -494,8 +518,8 @@ func (client ManagedClustersClient) ListComplete(ctx context.Context) (result Ma
|
||||||
|
|
||||||
// ListByResourceGroup lists managed clusters in the specified subscription and resource group. The operation returns
|
// ListByResourceGroup lists managed clusters in the specified subscription and resource group. The operation returns
|
||||||
// properties of each managed cluster.
|
// properties of each managed cluster.
|
||||||
//
|
// Parameters:
|
||||||
// resourceGroupName is the name of the resource group.
|
// resourceGroupName - the name of the resource group.
|
||||||
func (client ManagedClustersClient) ListByResourceGroup(ctx context.Context, resourceGroupName string) (result ManagedClusterListResultPage, err error) {
|
func (client ManagedClustersClient) ListByResourceGroup(ctx context.Context, resourceGroupName string) (result ManagedClusterListResultPage, err error) {
|
||||||
result.fn = client.listByResourceGroupNextResults
|
result.fn = client.listByResourceGroupNextResults
|
||||||
req, err := client.ListByResourceGroupPreparer(ctx, resourceGroupName)
|
req, err := client.ListByResourceGroupPreparer(ctx, resourceGroupName)
|
||||||
|
|
@ -526,7 +550,7 @@ func (client ManagedClustersClient) ListByResourceGroupPreparer(ctx context.Cont
|
||||||
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
"subscriptionId": autorest.Encode("path", client.SubscriptionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
const APIVersion = "2017-08-31"
|
const APIVersion = "2018-03-31"
|
||||||
queryParameters := map[string]interface{}{
|
queryParameters := map[string]interface{}{
|
||||||
"api-version": APIVersion,
|
"api-version": APIVersion,
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,98 @@
|
||||||
|
package containerservice
|
||||||
|
|
||||||
|
// Copyright (c) Microsoft and contributors. All rights reserved.
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// Code generated by Microsoft (R) AutoRest Code Generator.
|
||||||
|
// Changes may cause incorrect behavior and will be lost if the code is regenerated.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
"github.com/Azure/go-autorest/autorest/azure"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OperationsClient is the the Container Service Client.
|
||||||
|
type OperationsClient struct {
|
||||||
|
BaseClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOperationsClient creates an instance of the OperationsClient client.
|
||||||
|
func NewOperationsClient(subscriptionID string) OperationsClient {
|
||||||
|
return NewOperationsClientWithBaseURI(DefaultBaseURI, subscriptionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOperationsClientWithBaseURI creates an instance of the OperationsClient client.
|
||||||
|
func NewOperationsClientWithBaseURI(baseURI string, subscriptionID string) OperationsClient {
|
||||||
|
return OperationsClient{NewWithBaseURI(baseURI, subscriptionID)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List gets a list of compute operations.
|
||||||
|
func (client OperationsClient) List(ctx context.Context) (result OperationListResult, err error) {
|
||||||
|
req, err := client.ListPreparer(ctx)
|
||||||
|
if err != nil {
|
||||||
|
err = autorest.NewErrorWithError(err, "containerservice.OperationsClient", "List", nil, "Failure preparing request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.ListSender(req)
|
||||||
|
if err != nil {
|
||||||
|
result.Response = autorest.Response{Response: resp}
|
||||||
|
err = autorest.NewErrorWithError(err, "containerservice.OperationsClient", "List", resp, "Failure sending request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = client.ListResponder(resp)
|
||||||
|
if err != nil {
|
||||||
|
err = autorest.NewErrorWithError(err, "containerservice.OperationsClient", "List", resp, "Failure responding to request")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListPreparer prepares the List request.
|
||||||
|
func (client OperationsClient) ListPreparer(ctx context.Context) (*http.Request, error) {
|
||||||
|
const APIVersion = "2018-03-31"
|
||||||
|
queryParameters := map[string]interface{}{
|
||||||
|
"api-version": APIVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
preparer := autorest.CreatePreparer(
|
||||||
|
autorest.AsGet(),
|
||||||
|
autorest.WithBaseURL(client.BaseURI),
|
||||||
|
autorest.WithPath("/providers/Microsoft.ContainerService/operations"),
|
||||||
|
autorest.WithQueryParameters(queryParameters))
|
||||||
|
return preparer.Prepare((&http.Request{}).WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListSender sends the List request. The method will close the
|
||||||
|
// http.Response Body if it receives an error.
|
||||||
|
func (client OperationsClient) ListSender(req *http.Request) (*http.Response, error) {
|
||||||
|
return autorest.SendWithSender(client, req,
|
||||||
|
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListResponder handles the response to the List request. The method always
|
||||||
|
// closes the http.Response Body.
|
||||||
|
func (client OperationsClient) ListResponder(resp *http.Response) (result OperationListResult, err error) {
|
||||||
|
err = autorest.Respond(
|
||||||
|
resp,
|
||||||
|
client.ByInspecting(),
|
||||||
|
azure.WithErrorUnlessStatusCode(http.StatusOK),
|
||||||
|
autorest.ByUnmarshallingJSON(&result),
|
||||||
|
autorest.ByClosing())
|
||||||
|
result.Response = autorest.Response{Response: resp}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package containerservice
|
package containerservice
|
||||||
|
|
||||||
|
import "github.com/Azure/azure-sdk-for-go/version"
|
||||||
|
|
||||||
// Copyright (c) Microsoft and contributors. All rights reserved.
|
// Copyright (c) Microsoft and contributors. All rights reserved.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
|
@ -19,10 +21,10 @@ package containerservice
|
||||||
|
|
||||||
// UserAgent returns the UserAgent string to use when sending http.Requests.
|
// UserAgent returns the UserAgent string to use when sending http.Requests.
|
||||||
func UserAgent() string {
|
func UserAgent() string {
|
||||||
return "Azure-SDK-For-Go/v12.4.0-beta services"
|
return "Azure-SDK-For-Go/" + version.Number + " containerservice/2018-03-31"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version returns the semantic version (see http://semver.org) of the client.
|
// Version returns the semantic version (see http://semver.org) of the client.
|
||||||
func Version() string {
|
func Version() string {
|
||||||
return "v12.4.0-beta"
|
return version.Number
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,191 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
Copyright 2015 Microsoft Corporation
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -0,0 +1,292 @@
|
||||||
|
# Azure Active Directory authentication for Go
|
||||||
|
|
||||||
|
This is a standalone package for authenticating with Azure Active
|
||||||
|
Directory from other Go libraries and applications, in particular the [Azure SDK
|
||||||
|
for Go](https://github.com/Azure/azure-sdk-for-go).
|
||||||
|
|
||||||
|
Note: Despite the package's name it is not related to other "ADAL" libraries
|
||||||
|
maintained in the [github.com/AzureAD](https://github.com/AzureAD) org. Issues
|
||||||
|
should be opened in [this repo's](https://github.com/Azure/go-autorest/issues)
|
||||||
|
or [the SDK's](https://github.com/Azure/azure-sdk-for-go/issues) issue
|
||||||
|
trackers.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get -u github.com/Azure/go-autorest/autorest/adal
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
An Active Directory application is required in order to use this library. An application can be registered in the [Azure Portal](https://portal.azure.com/) by following these [guidelines](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-integrating-applications) or using the [Azure CLI](https://github.com/Azure/azure-cli).
|
||||||
|
|
||||||
|
### Register an Azure AD Application with secret
|
||||||
|
|
||||||
|
|
||||||
|
1. Register a new application with a `secret` credential
|
||||||
|
|
||||||
|
```
|
||||||
|
az ad app create \
|
||||||
|
--display-name example-app \
|
||||||
|
--homepage https://example-app/home \
|
||||||
|
--identifier-uris https://example-app/app \
|
||||||
|
--password secret
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create a service principal using the `Application ID` from previous step
|
||||||
|
|
||||||
|
```
|
||||||
|
az ad sp create --id "Application ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
* Replace `Application ID` with `appId` from step 1.
|
||||||
|
|
||||||
|
### Register an Azure AD Application with certificate
|
||||||
|
|
||||||
|
1. Create a private key
|
||||||
|
|
||||||
|
```
|
||||||
|
openssl genrsa -out "example-app.key" 2048
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create the certificate
|
||||||
|
|
||||||
|
```
|
||||||
|
openssl req -new -key "example-app.key" -subj "/CN=example-app" -out "example-app.csr"
|
||||||
|
openssl x509 -req -in "example-app.csr" -signkey "example-app.key" -out "example-app.crt" -days 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create the PKCS12 version of the certificate containing also the private key
|
||||||
|
|
||||||
|
```
|
||||||
|
openssl pkcs12 -export -out "example-app.pfx" -inkey "example-app.key" -in "example-app.crt" -passout pass:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Register a new application with the certificate content form `example-app.crt`
|
||||||
|
|
||||||
|
```
|
||||||
|
certificateContents="$(tail -n+2 "example-app.crt" | head -n-1)"
|
||||||
|
|
||||||
|
az ad app create \
|
||||||
|
--display-name example-app \
|
||||||
|
--homepage https://example-app/home \
|
||||||
|
--identifier-uris https://example-app/app \
|
||||||
|
--key-usage Verify --end-date 2018-01-01 \
|
||||||
|
--key-value "${certificateContents}"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Create a service principal using the `Application ID` from previous step
|
||||||
|
|
||||||
|
```
|
||||||
|
az ad sp create --id "APPLICATION_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
* Replace `APPLICATION_ID` with `appId` from step 4.
|
||||||
|
|
||||||
|
|
||||||
|
### Grant the necessary permissions
|
||||||
|
|
||||||
|
Azure relies on a Role-Based Access Control (RBAC) model to manage the access to resources at a fine-grained
|
||||||
|
level. There is a set of [pre-defined roles](https://docs.microsoft.com/en-us/azure/active-directory/role-based-access-built-in-roles)
|
||||||
|
which can be assigned to a service principal of an Azure AD application depending of your needs.
|
||||||
|
|
||||||
|
```
|
||||||
|
az role assignment create --assigner "SERVICE_PRINCIPAL_ID" --role "ROLE_NAME"
|
||||||
|
```
|
||||||
|
|
||||||
|
* Replace the `SERVICE_PRINCIPAL_ID` with the `appId` from previous step.
|
||||||
|
* Replace the `ROLE_NAME` with a role name of your choice.
|
||||||
|
|
||||||
|
It is also possible to define custom role definitions.
|
||||||
|
|
||||||
|
```
|
||||||
|
az role definition create --role-definition role-definition.json
|
||||||
|
```
|
||||||
|
|
||||||
|
* Check [custom roles](https://docs.microsoft.com/en-us/azure/active-directory/role-based-access-control-custom-roles) for more details regarding the content of `role-definition.json` file.
|
||||||
|
|
||||||
|
|
||||||
|
### Acquire Access Token
|
||||||
|
|
||||||
|
The common configuration used by all flows:
|
||||||
|
|
||||||
|
```Go
|
||||||
|
const activeDirectoryEndpoint = "https://login.microsoftonline.com/"
|
||||||
|
tenantID := "TENANT_ID"
|
||||||
|
oauthConfig, err := adal.NewOAuthConfig(activeDirectoryEndpoint, tenantID)
|
||||||
|
|
||||||
|
applicationID := "APPLICATION_ID"
|
||||||
|
|
||||||
|
callback := func(token adal.Token) error {
|
||||||
|
// This is called after the token is acquired
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resource for which the token is acquired
|
||||||
|
resource := "https://management.core.windows.net/"
|
||||||
|
```
|
||||||
|
|
||||||
|
* Replace the `TENANT_ID` with your tenant ID.
|
||||||
|
* Replace the `APPLICATION_ID` with the value from previous section.
|
||||||
|
|
||||||
|
#### Client Credentials
|
||||||
|
|
||||||
|
```Go
|
||||||
|
applicationSecret := "APPLICATION_SECRET"
|
||||||
|
|
||||||
|
spt, err := adal.NewServicePrincipalToken(
|
||||||
|
oauthConfig,
|
||||||
|
appliationID,
|
||||||
|
applicationSecret,
|
||||||
|
resource,
|
||||||
|
callbacks...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire a new access token
|
||||||
|
err = spt.Refresh()
|
||||||
|
if (err == nil) {
|
||||||
|
token := spt.Token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Replace the `APPLICATION_SECRET` with the `password` value from previous section.
|
||||||
|
|
||||||
|
#### Client Certificate
|
||||||
|
|
||||||
|
```Go
|
||||||
|
certificatePath := "./example-app.pfx"
|
||||||
|
|
||||||
|
certData, err := ioutil.ReadFile(certificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", certificatePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the certificate and private key from pfx file
|
||||||
|
certificate, rsaPrivateKey, err := decodePkcs12(certData, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spt, err := adal.NewServicePrincipalTokenFromCertificate(
|
||||||
|
oauthConfig,
|
||||||
|
applicationID,
|
||||||
|
certificate,
|
||||||
|
rsaPrivateKey,
|
||||||
|
resource,
|
||||||
|
callbacks...)
|
||||||
|
|
||||||
|
// Acquire a new access token
|
||||||
|
err = spt.Refresh()
|
||||||
|
if (err == nil) {
|
||||||
|
token := spt.Token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* Update the certificate path to point to the example-app.pfx file which was created in previous section.
|
||||||
|
|
||||||
|
|
||||||
|
#### Device Code
|
||||||
|
|
||||||
|
```Go
|
||||||
|
oauthClient := &http.Client{}
|
||||||
|
|
||||||
|
// Acquire the device code
|
||||||
|
deviceCode, err := adal.InitiateDeviceAuth(
|
||||||
|
oauthClient,
|
||||||
|
oauthConfig,
|
||||||
|
applicationID,
|
||||||
|
resource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to start device auth flow: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the authentication message
|
||||||
|
fmt.Println(*deviceCode.Message)
|
||||||
|
|
||||||
|
// Wait here until the user is authenticated
|
||||||
|
token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to finish device auth flow: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spt, err := adal.NewServicePrincipalTokenFromManualToken(
|
||||||
|
oauthConfig,
|
||||||
|
applicationID,
|
||||||
|
resource,
|
||||||
|
*token,
|
||||||
|
callbacks...)
|
||||||
|
|
||||||
|
if (err == nil) {
|
||||||
|
token := spt.Token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Username password authenticate
|
||||||
|
|
||||||
|
```Go
|
||||||
|
spt, err := adal.NewServicePrincipalTokenFromUsernamePassword(
|
||||||
|
oauthConfig,
|
||||||
|
applicationID,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
resource,
|
||||||
|
callbacks...)
|
||||||
|
|
||||||
|
if (err == nil) {
|
||||||
|
token := spt.Token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Authorization code authenticate
|
||||||
|
|
||||||
|
``` Go
|
||||||
|
spt, err := adal.NewServicePrincipalTokenFromAuthorizationCode(
|
||||||
|
oauthConfig,
|
||||||
|
applicationID,
|
||||||
|
clientSecret,
|
||||||
|
authorizationCode,
|
||||||
|
redirectURI,
|
||||||
|
resource,
|
||||||
|
callbacks...)
|
||||||
|
|
||||||
|
err = spt.Refresh()
|
||||||
|
if (err == nil) {
|
||||||
|
token := spt.Token
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line Tool
|
||||||
|
|
||||||
|
A command line tool is available in `cmd/adal.go` that can acquire a token for a given resource. It supports all flows mentioned above.
|
||||||
|
|
||||||
|
```
|
||||||
|
adal -h
|
||||||
|
|
||||||
|
Usage of ./adal:
|
||||||
|
-applicationId string
|
||||||
|
application id
|
||||||
|
-certificatePath string
|
||||||
|
path to pk12/PFC application certificate
|
||||||
|
-mode string
|
||||||
|
authentication mode (device, secret, cert, refresh) (default "device")
|
||||||
|
-resource string
|
||||||
|
resource for which the token is requested
|
||||||
|
-secret string
|
||||||
|
application secret
|
||||||
|
-tenantId string
|
||||||
|
tenant id
|
||||||
|
-tokenCachePath string
|
||||||
|
location of oath token cache (default "/home/cgc/.adal/accessToken.json")
|
||||||
|
```
|
||||||
|
|
||||||
|
Example acquire a token for `https://management.core.windows.net/` using device code flow:
|
||||||
|
|
||||||
|
```
|
||||||
|
adal -mode device \
|
||||||
|
-applicationId "APPLICATION_ID" \
|
||||||
|
-tenantId "TENANT_ID" \
|
||||||
|
-resource https://management.core.windows.net/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package adal
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
activeDirectoryAPIVersion = "1.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuthConfig represents the endpoints needed
|
||||||
|
// in OAuth operations
|
||||||
|
type OAuthConfig struct {
|
||||||
|
AuthorityEndpoint url.URL `json:"authorityEndpoint"`
|
||||||
|
AuthorizeEndpoint url.URL `json:"authorizeEndpoint"`
|
||||||
|
TokenEndpoint url.URL `json:"tokenEndpoint"`
|
||||||
|
DeviceCodeEndpoint url.URL `json:"deviceCodeEndpoint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns true if the OAuthConfig object is zero-initialized.
|
||||||
|
func (oac OAuthConfig) IsZero() bool {
|
||||||
|
return oac == OAuthConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateStringParam(param, name string) error {
|
||||||
|
if len(param) == 0 {
|
||||||
|
return fmt.Errorf("parameter '" + name + "' cannot be empty")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOAuthConfig returns an OAuthConfig with tenant specific urls
|
||||||
|
func NewOAuthConfig(activeDirectoryEndpoint, tenantID string) (*OAuthConfig, error) {
|
||||||
|
if err := validateStringParam(activeDirectoryEndpoint, "activeDirectoryEndpoint"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// it's legal for tenantID to be empty so don't validate it
|
||||||
|
const activeDirectoryEndpointTemplate = "%s/oauth2/%s?api-version=%s"
|
||||||
|
u, err := url.Parse(activeDirectoryEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authorityURL, err := u.Parse(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authorizeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "authorize", activeDirectoryAPIVersion))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokenURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "token", activeDirectoryAPIVersion))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
deviceCodeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "devicecode", activeDirectoryAPIVersion))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OAuthConfig{
|
||||||
|
AuthorityEndpoint: *authorityURL,
|
||||||
|
AuthorizeEndpoint: *authorizeURL,
|
||||||
|
TokenEndpoint: *tokenURL,
|
||||||
|
DeviceCodeEndpoint: *deviceCodeURL,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,242 @@
|
||||||
|
package adal
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
/*
|
||||||
|
This file is largely based on rjw57/oauth2device's code, with the follow differences:
|
||||||
|
* scope -> resource, and only allow a single one
|
||||||
|
* receive "Message" in the DeviceCode struct and show it to users as the prompt
|
||||||
|
* azure-xplat-cli has the following behavior that this emulates:
|
||||||
|
- does not send client_secret during the token exchange
|
||||||
|
- sends resource again in the token exchange request
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logPrefix = "autorest/adal/devicetoken:"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrDeviceGeneric represents an unknown error from the token endpoint when using device flow
|
||||||
|
ErrDeviceGeneric = fmt.Errorf("%s Error while retrieving OAuth token: Unknown Error", logPrefix)
|
||||||
|
|
||||||
|
// ErrDeviceAccessDenied represents an access denied error from the token endpoint when using device flow
|
||||||
|
ErrDeviceAccessDenied = fmt.Errorf("%s Error while retrieving OAuth token: Access Denied", logPrefix)
|
||||||
|
|
||||||
|
// ErrDeviceAuthorizationPending represents the server waiting on the user to complete the device flow
|
||||||
|
ErrDeviceAuthorizationPending = fmt.Errorf("%s Error while retrieving OAuth token: Authorization Pending", logPrefix)
|
||||||
|
|
||||||
|
// ErrDeviceCodeExpired represents the server timing out and expiring the code during device flow
|
||||||
|
ErrDeviceCodeExpired = fmt.Errorf("%s Error while retrieving OAuth token: Code Expired", logPrefix)
|
||||||
|
|
||||||
|
// ErrDeviceSlowDown represents the service telling us we're polling too often during device flow
|
||||||
|
ErrDeviceSlowDown = fmt.Errorf("%s Error while retrieving OAuth token: Slow Down", logPrefix)
|
||||||
|
|
||||||
|
// ErrDeviceCodeEmpty represents an empty device code from the device endpoint while using device flow
|
||||||
|
ErrDeviceCodeEmpty = fmt.Errorf("%s Error while retrieving device code: Device Code Empty", logPrefix)
|
||||||
|
|
||||||
|
// ErrOAuthTokenEmpty represents an empty OAuth token from the token endpoint when using device flow
|
||||||
|
ErrOAuthTokenEmpty = fmt.Errorf("%s Error while retrieving OAuth token: Token Empty", logPrefix)
|
||||||
|
|
||||||
|
errCodeSendingFails = "Error occurred while sending request for Device Authorization Code"
|
||||||
|
errCodeHandlingFails = "Error occurred while handling response from the Device Endpoint"
|
||||||
|
errTokenSendingFails = "Error occurred while sending request with device code for a token"
|
||||||
|
errTokenHandlingFails = "Error occurred while handling response from the Token Endpoint (during device flow)"
|
||||||
|
errStatusNotOK = "Error HTTP status != 200"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeviceCode is the object returned by the device auth endpoint
|
||||||
|
// It contains information to instruct the user to complete the auth flow
|
||||||
|
type DeviceCode struct {
|
||||||
|
DeviceCode *string `json:"device_code,omitempty"`
|
||||||
|
UserCode *string `json:"user_code,omitempty"`
|
||||||
|
VerificationURL *string `json:"verification_url,omitempty"`
|
||||||
|
ExpiresIn *int64 `json:"expires_in,string,omitempty"`
|
||||||
|
Interval *int64 `json:"interval,string,omitempty"`
|
||||||
|
|
||||||
|
Message *string `json:"message"` // Azure specific
|
||||||
|
Resource string // store the following, stored when initiating, used when exchanging
|
||||||
|
OAuthConfig OAuthConfig
|
||||||
|
ClientID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenError is the object returned by the token exchange endpoint
|
||||||
|
// when something is amiss
|
||||||
|
type TokenError struct {
|
||||||
|
Error *string `json:"error,omitempty"`
|
||||||
|
ErrorCodes []int `json:"error_codes,omitempty"`
|
||||||
|
ErrorDescription *string `json:"error_description,omitempty"`
|
||||||
|
Timestamp *string `json:"timestamp,omitempty"`
|
||||||
|
TraceID *string `json:"trace_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceToken is the object return by the token exchange endpoint
|
||||||
|
// It can either look like a Token or an ErrorToken, so put both here
|
||||||
|
// and check for presence of "Error" to know if we are in error state
|
||||||
|
type deviceToken struct {
|
||||||
|
Token
|
||||||
|
TokenError
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitiateDeviceAuth initiates a device auth flow. It returns a DeviceCode
|
||||||
|
// that can be used with CheckForUserCompletion or WaitForUserCompletion.
|
||||||
|
func InitiateDeviceAuth(sender Sender, oauthConfig OAuthConfig, clientID, resource string) (*DeviceCode, error) {
|
||||||
|
v := url.Values{
|
||||||
|
"client_id": []string{clientID},
|
||||||
|
"resource": []string{resource},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := v.Encode()
|
||||||
|
body := ioutil.NopCloser(strings.NewReader(s))
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, oauthConfig.DeviceCodeEndpoint.String(), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeSendingFails, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ContentLength = int64(len(s))
|
||||||
|
req.Header.Set(contentType, mimeTypeFormPost)
|
||||||
|
resp, err := sender.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeSendingFails, err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
rb, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, errStatusNotOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(strings.Trim(string(rb), " ")) == 0 {
|
||||||
|
return nil, ErrDeviceCodeEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var code DeviceCode
|
||||||
|
err = json.Unmarshal(rb, &code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errCodeHandlingFails, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
code.ClientID = clientID
|
||||||
|
code.Resource = resource
|
||||||
|
code.OAuthConfig = oauthConfig
|
||||||
|
|
||||||
|
return &code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckForUserCompletion takes a DeviceCode and checks with the Azure AD OAuth endpoint
|
||||||
|
// to see if the device flow has: been completed, timed out, or otherwise failed
|
||||||
|
func CheckForUserCompletion(sender Sender, code *DeviceCode) (*Token, error) {
|
||||||
|
v := url.Values{
|
||||||
|
"client_id": []string{code.ClientID},
|
||||||
|
"code": []string{*code.DeviceCode},
|
||||||
|
"grant_type": []string{OAuthGrantTypeDeviceCode},
|
||||||
|
"resource": []string{code.Resource},
|
||||||
|
}
|
||||||
|
|
||||||
|
s := v.Encode()
|
||||||
|
body := ioutil.NopCloser(strings.NewReader(s))
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, code.OAuthConfig.TokenEndpoint.String(), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenSendingFails, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ContentLength = int64(len(s))
|
||||||
|
req.Header.Set(contentType, mimeTypeFormPost)
|
||||||
|
resp, err := sender.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenSendingFails, err.Error())
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
rb, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && len(strings.Trim(string(rb), " ")) == 0 {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, errStatusNotOK)
|
||||||
|
}
|
||||||
|
if len(strings.Trim(string(rb), " ")) == 0 {
|
||||||
|
return nil, ErrOAuthTokenEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
var token deviceToken
|
||||||
|
err = json.Unmarshal(rb, &token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s %s: %s", logPrefix, errTokenHandlingFails, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.Error == nil {
|
||||||
|
return &token.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch *token.Error {
|
||||||
|
case "authorization_pending":
|
||||||
|
return nil, ErrDeviceAuthorizationPending
|
||||||
|
case "slow_down":
|
||||||
|
return nil, ErrDeviceSlowDown
|
||||||
|
case "access_denied":
|
||||||
|
return nil, ErrDeviceAccessDenied
|
||||||
|
case "code_expired":
|
||||||
|
return nil, ErrDeviceCodeExpired
|
||||||
|
default:
|
||||||
|
return nil, ErrDeviceGeneric
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForUserCompletion calls CheckForUserCompletion repeatedly until a token is granted or an error state occurs.
|
||||||
|
// This prevents the user from looping and checking against 'ErrDeviceAuthorizationPending'.
|
||||||
|
func WaitForUserCompletion(sender Sender, code *DeviceCode) (*Token, error) {
|
||||||
|
intervalDuration := time.Duration(*code.Interval) * time.Second
|
||||||
|
waitDuration := intervalDuration
|
||||||
|
|
||||||
|
for {
|
||||||
|
token, err := CheckForUserCompletion(sender, code)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch err {
|
||||||
|
case ErrDeviceSlowDown:
|
||||||
|
waitDuration += waitDuration
|
||||||
|
case ErrDeviceAuthorizationPending:
|
||||||
|
// noop
|
||||||
|
default: // everything else is "fatal" to us
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if waitDuration > (intervalDuration * 3) {
|
||||||
|
return nil, fmt.Errorf("%s Error waiting for user to complete device flow. Server told us to slow_down too much", logPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(waitDuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package adal
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadToken restores a Token object from a file located at 'path'.
|
||||||
|
func LoadToken(path string) (*Token, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var token Token
|
||||||
|
|
||||||
|
dec := json.NewDecoder(file)
|
||||||
|
if err = dec.Decode(&token); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode contents of file (%s) into Token representation: %v", path, err)
|
||||||
|
}
|
||||||
|
return &token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveToken persists an oauth token at the given location on disk.
|
||||||
|
// It moves the new file into place so it can safely be used to replace an existing file
|
||||||
|
// that maybe accessed by multiple processes.
|
||||||
|
func SaveToken(path string, mode os.FileMode, token Token) error {
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
err := os.MkdirAll(dir, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create directory (%s) to store token in: %v", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newFile, err := ioutil.TempFile(dir, "token")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create the temp file to write the token: %v", err)
|
||||||
|
}
|
||||||
|
tempPath := newFile.Name()
|
||||||
|
|
||||||
|
if err := json.NewEncoder(newFile).Encode(token); err != nil {
|
||||||
|
return fmt.Errorf("failed to encode token to file (%s) while saving token: %v", tempPath, err)
|
||||||
|
}
|
||||||
|
if err := newFile.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close temp file %s: %v", tempPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic replace to avoid multi-writer file corruptions
|
||||||
|
if err := os.Rename(tempPath, path); err != nil {
|
||||||
|
return fmt.Errorf("failed to move temporary token to desired output location. src=%s dst=%s: %v", tempPath, path, err)
|
||||||
|
}
|
||||||
|
if err := os.Chmod(path, mode); err != nil {
|
||||||
|
return fmt.Errorf("failed to chmod the token file %s: %v", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
package adal
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
contentType = "Content-Type"
|
||||||
|
mimeTypeFormPost = "application/x-www-form-urlencoded"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sender is the interface that wraps the Do method to send HTTP requests.
|
||||||
|
//
|
||||||
|
// The standard http.Client conforms to this interface.
|
||||||
|
type Sender interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SenderFunc is a method that implements the Sender interface.
|
||||||
|
type SenderFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
// Do implements the Sender interface on SenderFunc.
|
||||||
|
func (sf SenderFunc) Do(r *http.Request) (*http.Response, error) {
|
||||||
|
return sf(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendDecorator takes and possibility decorates, by wrapping, a Sender. Decorators may affect the
|
||||||
|
// http.Request and pass it along or, first, pass the http.Request along then react to the
|
||||||
|
// http.Response result.
|
||||||
|
type SendDecorator func(Sender) Sender
|
||||||
|
|
||||||
|
// CreateSender creates, decorates, and returns, as a Sender, the default http.Client.
|
||||||
|
func CreateSender(decorators ...SendDecorator) Sender {
|
||||||
|
return DecorateSender(&http.Client{}, decorators...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecorateSender accepts a Sender and a, possibly empty, set of SendDecorators, which is applies to
|
||||||
|
// the Sender. Decorators are applied in the order received, but their affect upon the request
|
||||||
|
// depends on whether they are a pre-decorator (change the http.Request and then pass it along) or a
|
||||||
|
// post-decorator (pass the http.Request along and react to the results in http.Response).
|
||||||
|
func DecorateSender(s Sender, decorators ...SendDecorator) Sender {
|
||||||
|
for _, decorate := range decorators {
|
||||||
|
s = decorate(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,966 @@
|
||||||
|
package adal
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/go-autorest/autorest/date"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultRefresh = 5 * time.Minute
|
||||||
|
|
||||||
|
// OAuthGrantTypeDeviceCode is the "grant_type" identifier used in device flow
|
||||||
|
OAuthGrantTypeDeviceCode = "device_code"
|
||||||
|
|
||||||
|
// OAuthGrantTypeClientCredentials is the "grant_type" identifier used in credential flows
|
||||||
|
OAuthGrantTypeClientCredentials = "client_credentials"
|
||||||
|
|
||||||
|
// OAuthGrantTypeUserPass is the "grant_type" identifier used in username and password auth flows
|
||||||
|
OAuthGrantTypeUserPass = "password"
|
||||||
|
|
||||||
|
// OAuthGrantTypeRefreshToken is the "grant_type" identifier used in refresh token flows
|
||||||
|
OAuthGrantTypeRefreshToken = "refresh_token"
|
||||||
|
|
||||||
|
// OAuthGrantTypeAuthorizationCode is the "grant_type" identifier used in authorization code flows
|
||||||
|
OAuthGrantTypeAuthorizationCode = "authorization_code"
|
||||||
|
|
||||||
|
// metadataHeader is the header required by MSI extension
|
||||||
|
metadataHeader = "Metadata"
|
||||||
|
|
||||||
|
// msiEndpoint is the well known endpoint for getting MSI authentications tokens
|
||||||
|
msiEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
|
||||||
|
|
||||||
|
// the default number of attempts to refresh an MSI authentication token
|
||||||
|
defaultMaxMSIRefreshAttempts = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuthTokenProvider is an interface which should be implemented by an access token retriever
|
||||||
|
type OAuthTokenProvider interface {
|
||||||
|
OAuthToken() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenRefreshError is an interface used by errors returned during token refresh.
|
||||||
|
type TokenRefreshError interface {
|
||||||
|
error
|
||||||
|
Response() *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresher is an interface for token refresh functionality
|
||||||
|
type Refresher interface {
|
||||||
|
Refresh() error
|
||||||
|
RefreshExchange(resource string) error
|
||||||
|
EnsureFresh() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefresherWithContext is an interface for token refresh functionality
|
||||||
|
type RefresherWithContext interface {
|
||||||
|
RefreshWithContext(ctx context.Context) error
|
||||||
|
RefreshExchangeWithContext(ctx context.Context, resource string) error
|
||||||
|
EnsureFreshWithContext(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenRefreshCallback is the type representing callbacks that will be called after
|
||||||
|
// a successful token refresh
|
||||||
|
type TokenRefreshCallback func(Token) error
|
||||||
|
|
||||||
|
// Token encapsulates the access token used to authorize Azure requests.
|
||||||
|
type Token struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
|
||||||
|
ExpiresIn string `json:"expires_in"`
|
||||||
|
ExpiresOn string `json:"expires_on"`
|
||||||
|
NotBefore string `json:"not_before"`
|
||||||
|
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
Type string `json:"token_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns true if the token object is zero-initialized.
|
||||||
|
func (t Token) IsZero() bool {
|
||||||
|
return t == Token{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expires returns the time.Time when the Token expires.
|
||||||
|
func (t Token) Expires() time.Time {
|
||||||
|
s, err := strconv.Atoi(t.ExpiresOn)
|
||||||
|
if err != nil {
|
||||||
|
s = -3600
|
||||||
|
}
|
||||||
|
|
||||||
|
expiration := date.NewUnixTimeFromSeconds(float64(s))
|
||||||
|
|
||||||
|
return time.Time(expiration).UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExpired returns true if the Token is expired, false otherwise.
|
||||||
|
func (t Token) IsExpired() bool {
|
||||||
|
return t.WillExpireIn(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WillExpireIn returns true if the Token will expire after the passed time.Duration interval
|
||||||
|
// from now, false otherwise.
|
||||||
|
func (t Token) WillExpireIn(d time.Duration) bool {
|
||||||
|
return !t.Expires().After(time.Now().Add(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
//OAuthToken return the current access token
|
||||||
|
func (t *Token) OAuthToken() string {
|
||||||
|
return t.AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePrincipalSecret is an interface that allows various secret mechanism to fill the form
|
||||||
|
// that is submitted when acquiring an oAuth token.
|
||||||
|
type ServicePrincipalSecret interface {
|
||||||
|
SetAuthenticationValues(spt *ServicePrincipalToken, values *url.Values) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePrincipalNoSecret represents a secret type that contains no secret
|
||||||
|
// meaning it is not valid for fetching a fresh token. This is used by Manual
|
||||||
|
type ServicePrincipalNoSecret struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret
|
||||||
|
// It only returns an error for the ServicePrincipalNoSecret type
|
||||||
|
func (noSecret *ServicePrincipalNoSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||||
|
return fmt.Errorf("Manually created ServicePrincipalToken does not contain secret material to retrieve a new access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (noSecret ServicePrincipalNoSecret) MarshalJSON() ([]byte, error) {
|
||||||
|
type tokenType struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
return json.Marshal(tokenType{
|
||||||
|
Type: "ServicePrincipalNoSecret",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePrincipalTokenSecret implements ServicePrincipalSecret for client_secret type authorization.
|
||||||
|
type ServicePrincipalTokenSecret struct {
|
||||||
|
ClientSecret string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
||||||
|
// It will populate the form submitted during oAuth Token Acquisition using the client_secret.
|
||||||
|
func (tokenSecret *ServicePrincipalTokenSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||||
|
v.Set("client_secret", tokenSecret.ClientSecret)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (tokenSecret ServicePrincipalTokenSecret) MarshalJSON() ([]byte, error) {
|
||||||
|
type tokenType struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
return json.Marshal(tokenType{
|
||||||
|
Type: "ServicePrincipalTokenSecret",
|
||||||
|
Value: tokenSecret.ClientSecret,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePrincipalCertificateSecret implements ServicePrincipalSecret for generic RSA cert auth with signed JWTs.
|
||||||
|
type ServicePrincipalCertificateSecret struct {
|
||||||
|
Certificate *x509.Certificate
|
||||||
|
PrivateKey *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignJwt returns the JWT signed with the certificate's private key.
|
||||||
|
func (secret *ServicePrincipalCertificateSecret) SignJwt(spt *ServicePrincipalToken) (string, error) {
|
||||||
|
hasher := sha1.New()
|
||||||
|
_, err := hasher.Write(secret.Certificate.Raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbprint := base64.URLEncoding.EncodeToString(hasher.Sum(nil))
|
||||||
|
|
||||||
|
// The jti (JWT ID) claim provides a unique identifier for the JWT.
|
||||||
|
jti := make([]byte, 20)
|
||||||
|
_, err = rand.Read(jti)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.New(jwt.SigningMethodRS256)
|
||||||
|
token.Header["x5t"] = thumbprint
|
||||||
|
token.Claims = jwt.MapClaims{
|
||||||
|
"aud": spt.inner.OauthConfig.TokenEndpoint.String(),
|
||||||
|
"iss": spt.inner.ClientID,
|
||||||
|
"sub": spt.inner.ClientID,
|
||||||
|
"jti": base64.URLEncoding.EncodeToString(jti),
|
||||||
|
"nbf": time.Now().Unix(),
|
||||||
|
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
signedString, err := token.SignedString(secret.PrivateKey)
|
||||||
|
return signedString, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
||||||
|
// It will populate the form submitted during oAuth Token Acquisition using a JWT signed with a certificate.
|
||||||
|
func (secret *ServicePrincipalCertificateSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||||
|
jwt, err := secret.SignJwt(spt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Set("client_assertion", jwt)
|
||||||
|
v.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (secret ServicePrincipalCertificateSecret) MarshalJSON() ([]byte, error) {
|
||||||
|
return nil, errors.New("marshalling ServicePrincipalCertificateSecret is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePrincipalMSISecret implements ServicePrincipalSecret for machines running the MSI Extension.
|
||||||
|
type ServicePrincipalMSISecret struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
||||||
|
func (msiSecret *ServicePrincipalMSISecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (msiSecret ServicePrincipalMSISecret) MarshalJSON() ([]byte, error) {
|
||||||
|
return nil, errors.New("marshalling ServicePrincipalMSISecret is not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePrincipalUsernamePasswordSecret implements ServicePrincipalSecret for username and password auth.
|
||||||
|
type ServicePrincipalUsernamePasswordSecret struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
||||||
|
func (secret *ServicePrincipalUsernamePasswordSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||||
|
v.Set("username", secret.Username)
|
||||||
|
v.Set("password", secret.Password)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (secret ServicePrincipalUsernamePasswordSecret) MarshalJSON() ([]byte, error) {
|
||||||
|
type tokenType struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
return json.Marshal(tokenType{
|
||||||
|
Type: "ServicePrincipalUsernamePasswordSecret",
|
||||||
|
Username: secret.Username,
|
||||||
|
Password: secret.Password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePrincipalAuthorizationCodeSecret implements ServicePrincipalSecret for authorization code auth.
|
||||||
|
type ServicePrincipalAuthorizationCodeSecret struct {
|
||||||
|
ClientSecret string `json:"value"`
|
||||||
|
AuthorizationCode string `json:"authCode"`
|
||||||
|
RedirectURI string `json:"redirect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
|
||||||
|
func (secret *ServicePrincipalAuthorizationCodeSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
|
||||||
|
v.Set("code", secret.AuthorizationCode)
|
||||||
|
v.Set("client_secret", secret.ClientSecret)
|
||||||
|
v.Set("redirect_uri", secret.RedirectURI)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (secret ServicePrincipalAuthorizationCodeSecret) MarshalJSON() ([]byte, error) {
|
||||||
|
type tokenType struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
AuthCode string `json:"authCode"`
|
||||||
|
Redirect string `json:"redirect"`
|
||||||
|
}
|
||||||
|
return json.Marshal(tokenType{
|
||||||
|
Type: "ServicePrincipalAuthorizationCodeSecret",
|
||||||
|
Value: secret.ClientSecret,
|
||||||
|
AuthCode: secret.AuthorizationCode,
|
||||||
|
Redirect: secret.RedirectURI,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServicePrincipalToken encapsulates a Token created for a Service Principal.
|
||||||
|
type ServicePrincipalToken struct {
|
||||||
|
inner servicePrincipalToken
|
||||||
|
refreshLock *sync.RWMutex
|
||||||
|
sender Sender
|
||||||
|
refreshCallbacks []TokenRefreshCallback
|
||||||
|
// MaxMSIRefreshAttempts is the maximum number of attempts to refresh an MSI token.
|
||||||
|
MaxMSIRefreshAttempts int
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalTokenJSON returns the marshalled inner token.
|
||||||
|
func (spt ServicePrincipalToken) MarshalTokenJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(spt.inner.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRefreshCallbacks replaces any existing refresh callbacks with the specified callbacks.
|
||||||
|
func (spt *ServicePrincipalToken) SetRefreshCallbacks(callbacks []TokenRefreshCallback) {
|
||||||
|
spt.refreshCallbacks = callbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (spt ServicePrincipalToken) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(spt.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||||
|
func (spt *ServicePrincipalToken) UnmarshalJSON(data []byte) error {
|
||||||
|
// need to determine the token type
|
||||||
|
raw := map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(data, &raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
secret := raw["secret"].(map[string]interface{})
|
||||||
|
switch secret["type"] {
|
||||||
|
case "ServicePrincipalNoSecret":
|
||||||
|
spt.inner.Secret = &ServicePrincipalNoSecret{}
|
||||||
|
case "ServicePrincipalTokenSecret":
|
||||||
|
spt.inner.Secret = &ServicePrincipalTokenSecret{}
|
||||||
|
case "ServicePrincipalCertificateSecret":
|
||||||
|
return errors.New("unmarshalling ServicePrincipalCertificateSecret is not supported")
|
||||||
|
case "ServicePrincipalMSISecret":
|
||||||
|
return errors.New("unmarshalling ServicePrincipalMSISecret is not supported")
|
||||||
|
case "ServicePrincipalUsernamePasswordSecret":
|
||||||
|
spt.inner.Secret = &ServicePrincipalUsernamePasswordSecret{}
|
||||||
|
case "ServicePrincipalAuthorizationCodeSecret":
|
||||||
|
spt.inner.Secret = &ServicePrincipalAuthorizationCodeSecret{}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unrecognized token type '%s'", secret["type"])
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(data, &spt.inner)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
spt.refreshLock = &sync.RWMutex{}
|
||||||
|
spt.sender = &http.Client{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal type used for marshalling/unmarshalling
|
||||||
|
type servicePrincipalToken struct {
|
||||||
|
Token Token `json:"token"`
|
||||||
|
Secret ServicePrincipalSecret `json:"secret"`
|
||||||
|
OauthConfig OAuthConfig `json:"oauth"`
|
||||||
|
ClientID string `json:"clientID"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
AutoRefresh bool `json:"autoRefresh"`
|
||||||
|
RefreshWithin time.Duration `json:"refreshWithin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOAuthConfig(oac OAuthConfig) error {
|
||||||
|
if oac.IsZero() {
|
||||||
|
return fmt.Errorf("parameter 'oauthConfig' cannot be zero-initialized")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalTokenWithSecret create a ServicePrincipalToken using the supplied ServicePrincipalSecret implementation.
|
||||||
|
func NewServicePrincipalTokenWithSecret(oauthConfig OAuthConfig, id string, resource string, secret ServicePrincipalSecret, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
if err := validateOAuthConfig(oauthConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(id, "id"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(resource, "resource"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if secret == nil {
|
||||||
|
return nil, fmt.Errorf("parameter 'secret' cannot be nil")
|
||||||
|
}
|
||||||
|
spt := &ServicePrincipalToken{
|
||||||
|
inner: servicePrincipalToken{
|
||||||
|
OauthConfig: oauthConfig,
|
||||||
|
Secret: secret,
|
||||||
|
ClientID: id,
|
||||||
|
Resource: resource,
|
||||||
|
AutoRefresh: true,
|
||||||
|
RefreshWithin: defaultRefresh,
|
||||||
|
},
|
||||||
|
refreshLock: &sync.RWMutex{},
|
||||||
|
sender: &http.Client{},
|
||||||
|
refreshCallbacks: callbacks,
|
||||||
|
}
|
||||||
|
return spt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalTokenFromManualToken creates a ServicePrincipalToken using the supplied token
|
||||||
|
func NewServicePrincipalTokenFromManualToken(oauthConfig OAuthConfig, clientID string, resource string, token Token, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
if err := validateOAuthConfig(oauthConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(clientID, "clientID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(resource, "resource"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if token.IsZero() {
|
||||||
|
return nil, fmt.Errorf("parameter 'token' cannot be zero-initialized")
|
||||||
|
}
|
||||||
|
spt, err := NewServicePrincipalTokenWithSecret(
|
||||||
|
oauthConfig,
|
||||||
|
clientID,
|
||||||
|
resource,
|
||||||
|
&ServicePrincipalNoSecret{},
|
||||||
|
callbacks...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
spt.inner.Token = token
|
||||||
|
|
||||||
|
return spt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalTokenFromManualTokenSecret creates a ServicePrincipalToken using the supplied token and secret
|
||||||
|
func NewServicePrincipalTokenFromManualTokenSecret(oauthConfig OAuthConfig, clientID string, resource string, token Token, secret ServicePrincipalSecret, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
if err := validateOAuthConfig(oauthConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(clientID, "clientID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(resource, "resource"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if secret == nil {
|
||||||
|
return nil, fmt.Errorf("parameter 'secret' cannot be nil")
|
||||||
|
}
|
||||||
|
if token.IsZero() {
|
||||||
|
return nil, fmt.Errorf("parameter 'token' cannot be zero-initialized")
|
||||||
|
}
|
||||||
|
spt, err := NewServicePrincipalTokenWithSecret(
|
||||||
|
oauthConfig,
|
||||||
|
clientID,
|
||||||
|
resource,
|
||||||
|
secret,
|
||||||
|
callbacks...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
spt.inner.Token = token
|
||||||
|
|
||||||
|
return spt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalToken creates a ServicePrincipalToken from the supplied Service Principal
|
||||||
|
// credentials scoped to the named resource.
|
||||||
|
func NewServicePrincipalToken(oauthConfig OAuthConfig, clientID string, secret string, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
if err := validateOAuthConfig(oauthConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(clientID, "clientID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(secret, "secret"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(resource, "resource"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewServicePrincipalTokenWithSecret(
|
||||||
|
oauthConfig,
|
||||||
|
clientID,
|
||||||
|
resource,
|
||||||
|
&ServicePrincipalTokenSecret{
|
||||||
|
ClientSecret: secret,
|
||||||
|
},
|
||||||
|
callbacks...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalTokenFromCertificate creates a ServicePrincipalToken from the supplied pkcs12 bytes.
|
||||||
|
func NewServicePrincipalTokenFromCertificate(oauthConfig OAuthConfig, clientID string, certificate *x509.Certificate, privateKey *rsa.PrivateKey, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
if err := validateOAuthConfig(oauthConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(clientID, "clientID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(resource, "resource"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if certificate == nil {
|
||||||
|
return nil, fmt.Errorf("parameter 'certificate' cannot be nil")
|
||||||
|
}
|
||||||
|
if privateKey == nil {
|
||||||
|
return nil, fmt.Errorf("parameter 'privateKey' cannot be nil")
|
||||||
|
}
|
||||||
|
return NewServicePrincipalTokenWithSecret(
|
||||||
|
oauthConfig,
|
||||||
|
clientID,
|
||||||
|
resource,
|
||||||
|
&ServicePrincipalCertificateSecret{
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
Certificate: certificate,
|
||||||
|
},
|
||||||
|
callbacks...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalTokenFromUsernamePassword creates a ServicePrincipalToken from the username and password.
|
||||||
|
func NewServicePrincipalTokenFromUsernamePassword(oauthConfig OAuthConfig, clientID string, username string, password string, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
if err := validateOAuthConfig(oauthConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(clientID, "clientID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(username, "username"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(password, "password"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(resource, "resource"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewServicePrincipalTokenWithSecret(
|
||||||
|
oauthConfig,
|
||||||
|
clientID,
|
||||||
|
resource,
|
||||||
|
&ServicePrincipalUsernamePasswordSecret{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
},
|
||||||
|
callbacks...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalTokenFromAuthorizationCode creates a ServicePrincipalToken from the
|
||||||
|
func NewServicePrincipalTokenFromAuthorizationCode(oauthConfig OAuthConfig, clientID string, clientSecret string, authorizationCode string, redirectURI string, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
|
||||||
|
if err := validateOAuthConfig(oauthConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(clientID, "clientID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(clientSecret, "clientSecret"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(authorizationCode, "authorizationCode"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(redirectURI, "redirectURI"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(resource, "resource"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewServicePrincipalTokenWithSecret(
|
||||||
|
oauthConfig,
|
||||||
|
clientID,
|
||||||
|
resource,
|
||||||
|
&ServicePrincipalAuthorizationCodeSecret{
|
||||||
|
ClientSecret: clientSecret,
|
||||||
|
AuthorizationCode: authorizationCode,
|
||||||
|
RedirectURI: redirectURI,
|
||||||
|
},
|
||||||
|
callbacks...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMSIVMEndpoint gets the MSI endpoint on Virtual Machines.
|
||||||
|
func GetMSIVMEndpoint() (string, error) {
|
||||||
|
return msiEndpoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalTokenFromMSI creates a ServicePrincipalToken via the MSI VM Extension.
|
||||||
|
// It will use the system assigned identity when creating the token.
|
||||||
|
func NewServicePrincipalTokenFromMSI(msiEndpoint, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
return newServicePrincipalTokenFromMSI(msiEndpoint, resource, nil, callbacks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServicePrincipalTokenFromMSIWithUserAssignedID creates a ServicePrincipalToken via the MSI VM Extension.
|
||||||
|
// It will use the specified user assigned identity when creating the token.
|
||||||
|
func NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, resource string, userAssignedID string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
return newServicePrincipalTokenFromMSI(msiEndpoint, resource, &userAssignedID, callbacks...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newServicePrincipalTokenFromMSI(msiEndpoint, resource string, userAssignedID *string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
|
||||||
|
if err := validateStringParam(msiEndpoint, "msiEndpoint"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := validateStringParam(resource, "resource"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if userAssignedID != nil {
|
||||||
|
if err := validateStringParam(*userAssignedID, "userAssignedID"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We set the oauth config token endpoint to be MSI's endpoint
|
||||||
|
msiEndpointURL, err := url.Parse(msiEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
v := url.Values{}
|
||||||
|
v.Set("resource", resource)
|
||||||
|
v.Set("api-version", "2018-02-01")
|
||||||
|
if userAssignedID != nil {
|
||||||
|
v.Set("client_id", *userAssignedID)
|
||||||
|
}
|
||||||
|
msiEndpointURL.RawQuery = v.Encode()
|
||||||
|
|
||||||
|
spt := &ServicePrincipalToken{
|
||||||
|
inner: servicePrincipalToken{
|
||||||
|
OauthConfig: OAuthConfig{
|
||||||
|
TokenEndpoint: *msiEndpointURL,
|
||||||
|
},
|
||||||
|
Secret: &ServicePrincipalMSISecret{},
|
||||||
|
Resource: resource,
|
||||||
|
AutoRefresh: true,
|
||||||
|
RefreshWithin: defaultRefresh,
|
||||||
|
},
|
||||||
|
refreshLock: &sync.RWMutex{},
|
||||||
|
sender: &http.Client{},
|
||||||
|
refreshCallbacks: callbacks,
|
||||||
|
MaxMSIRefreshAttempts: defaultMaxMSIRefreshAttempts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if userAssignedID != nil {
|
||||||
|
spt.inner.ClientID = *userAssignedID
|
||||||
|
}
|
||||||
|
|
||||||
|
return spt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal type that implements TokenRefreshError
|
||||||
|
type tokenRefreshError struct {
|
||||||
|
message string
|
||||||
|
resp *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface which is part of the TokenRefreshError interface.
|
||||||
|
func (tre tokenRefreshError) Error() string {
|
||||||
|
return tre.message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response implements the TokenRefreshError interface, it returns the raw HTTP response from the refresh operation.
|
||||||
|
func (tre tokenRefreshError) Response() *http.Response {
|
||||||
|
return tre.resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTokenRefreshError(message string, resp *http.Response) TokenRefreshError {
|
||||||
|
return tokenRefreshError{message: message, resp: resp}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureFresh will refresh the token if it will expire within the refresh window (as set by
|
||||||
|
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
|
||||||
|
func (spt *ServicePrincipalToken) EnsureFresh() error {
|
||||||
|
return spt.EnsureFreshWithContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by
|
||||||
|
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
|
||||||
|
func (spt *ServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error {
|
||||||
|
if spt.inner.AutoRefresh && spt.inner.Token.WillExpireIn(spt.inner.RefreshWithin) {
|
||||||
|
// take the write lock then check to see if the token was already refreshed
|
||||||
|
spt.refreshLock.Lock()
|
||||||
|
defer spt.refreshLock.Unlock()
|
||||||
|
if spt.inner.Token.WillExpireIn(spt.inner.RefreshWithin) {
|
||||||
|
return spt.refreshInternal(ctx, spt.inner.Resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvokeRefreshCallbacks calls any TokenRefreshCallbacks that were added to the SPT during initialization
|
||||||
|
func (spt *ServicePrincipalToken) InvokeRefreshCallbacks(token Token) error {
|
||||||
|
if spt.refreshCallbacks != nil {
|
||||||
|
for _, callback := range spt.refreshCallbacks {
|
||||||
|
err := callback(spt.inner.Token)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adal: TokenRefreshCallback handler failed. Error = '%v'", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh obtains a fresh token for the Service Principal.
|
||||||
|
// This method is not safe for concurrent use and should be syncrhonized.
|
||||||
|
func (spt *ServicePrincipalToken) Refresh() error {
|
||||||
|
return spt.RefreshWithContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshWithContext obtains a fresh token for the Service Principal.
|
||||||
|
// This method is not safe for concurrent use and should be syncrhonized.
|
||||||
|
func (spt *ServicePrincipalToken) RefreshWithContext(ctx context.Context) error {
|
||||||
|
spt.refreshLock.Lock()
|
||||||
|
defer spt.refreshLock.Unlock()
|
||||||
|
return spt.refreshInternal(ctx, spt.inner.Resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshExchange refreshes the token, but for a different resource.
|
||||||
|
// This method is not safe for concurrent use and should be syncrhonized.
|
||||||
|
func (spt *ServicePrincipalToken) RefreshExchange(resource string) error {
|
||||||
|
return spt.RefreshExchangeWithContext(context.Background(), resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefreshExchangeWithContext refreshes the token, but for a different resource.
|
||||||
|
// This method is not safe for concurrent use and should be syncrhonized.
|
||||||
|
func (spt *ServicePrincipalToken) RefreshExchangeWithContext(ctx context.Context, resource string) error {
|
||||||
|
spt.refreshLock.Lock()
|
||||||
|
defer spt.refreshLock.Unlock()
|
||||||
|
return spt.refreshInternal(ctx, resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (spt *ServicePrincipalToken) getGrantType() string {
|
||||||
|
switch spt.inner.Secret.(type) {
|
||||||
|
case *ServicePrincipalUsernamePasswordSecret:
|
||||||
|
return OAuthGrantTypeUserPass
|
||||||
|
case *ServicePrincipalAuthorizationCodeSecret:
|
||||||
|
return OAuthGrantTypeAuthorizationCode
|
||||||
|
default:
|
||||||
|
return OAuthGrantTypeClientCredentials
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIMDS(u url.URL) bool {
|
||||||
|
imds, err := url.Parse(msiEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return u.Host == imds.Host && u.Path == imds.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource string) error {
|
||||||
|
req, err := http.NewRequest(http.MethodPost, spt.inner.OauthConfig.TokenEndpoint.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adal: Failed to build the refresh request. Error = '%v'", err)
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
if !isIMDS(spt.inner.OauthConfig.TokenEndpoint) {
|
||||||
|
v := url.Values{}
|
||||||
|
v.Set("client_id", spt.inner.ClientID)
|
||||||
|
v.Set("resource", resource)
|
||||||
|
|
||||||
|
if spt.inner.Token.RefreshToken != "" {
|
||||||
|
v.Set("grant_type", OAuthGrantTypeRefreshToken)
|
||||||
|
v.Set("refresh_token", spt.inner.Token.RefreshToken)
|
||||||
|
// web apps must specify client_secret when refreshing tokens
|
||||||
|
// see https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code#refreshing-the-access-tokens
|
||||||
|
if spt.getGrantType() == OAuthGrantTypeAuthorizationCode {
|
||||||
|
err := spt.inner.Secret.SetAuthenticationValues(spt, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v.Set("grant_type", spt.getGrantType())
|
||||||
|
err := spt.inner.Secret.SetAuthenticationValues(spt, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := v.Encode()
|
||||||
|
body := ioutil.NopCloser(strings.NewReader(s))
|
||||||
|
req.ContentLength = int64(len(s))
|
||||||
|
req.Header.Set(contentType, mimeTypeFormPost)
|
||||||
|
req.Body = body
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := spt.inner.Secret.(*ServicePrincipalMSISecret); ok {
|
||||||
|
req.Method = http.MethodGet
|
||||||
|
req.Header.Set(metadataHeader, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp *http.Response
|
||||||
|
if isIMDS(spt.inner.OauthConfig.TokenEndpoint) {
|
||||||
|
resp, err = retryForIMDS(spt.sender, req, spt.MaxMSIRefreshAttempts)
|
||||||
|
} else {
|
||||||
|
resp, err = spt.sender.Do(req)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return newTokenRefreshError(fmt.Sprintf("adal: Failed to execute the refresh request. Error = '%v'", err), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
rb, err := ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if err != nil {
|
||||||
|
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Failed reading response body: %v", resp.StatusCode, err), resp)
|
||||||
|
}
|
||||||
|
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Response body: %s", resp.StatusCode, string(rb)), resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for the following error cases don't return a TokenRefreshError. the operation succeeded
|
||||||
|
// but some transient failure happened during deserialization. by returning a generic error
|
||||||
|
// the retry logic will kick in (we don't retry on TokenRefreshError).
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adal: Failed to read a new service principal token during refresh. Error = '%v'", err)
|
||||||
|
}
|
||||||
|
if len(strings.Trim(string(rb), " ")) == 0 {
|
||||||
|
return fmt.Errorf("adal: Empty service principal token received during refresh")
|
||||||
|
}
|
||||||
|
var token Token
|
||||||
|
err = json.Unmarshal(rb, &token)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("adal: Failed to unmarshal the service principal token during refresh. Error = '%v' JSON = '%s'", err, string(rb))
|
||||||
|
}
|
||||||
|
|
||||||
|
spt.inner.Token = token
|
||||||
|
|
||||||
|
return spt.InvokeRefreshCallbacks(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// retry logic specific to retrieving a token from the IMDS endpoint
|
||||||
|
func retryForIMDS(sender Sender, req *http.Request, maxAttempts int) (resp *http.Response, err error) {
|
||||||
|
// copied from client.go due to circular dependency
|
||||||
|
retries := []int{
|
||||||
|
http.StatusRequestTimeout, // 408
|
||||||
|
http.StatusTooManyRequests, // 429
|
||||||
|
http.StatusInternalServerError, // 500
|
||||||
|
http.StatusBadGateway, // 502
|
||||||
|
http.StatusServiceUnavailable, // 503
|
||||||
|
http.StatusGatewayTimeout, // 504
|
||||||
|
}
|
||||||
|
// extra retry status codes specific to IMDS
|
||||||
|
retries = append(retries,
|
||||||
|
http.StatusNotFound,
|
||||||
|
http.StatusGone,
|
||||||
|
// all remaining 5xx
|
||||||
|
http.StatusNotImplemented,
|
||||||
|
http.StatusHTTPVersionNotSupported,
|
||||||
|
http.StatusVariantAlsoNegotiates,
|
||||||
|
http.StatusInsufficientStorage,
|
||||||
|
http.StatusLoopDetected,
|
||||||
|
http.StatusNotExtended,
|
||||||
|
http.StatusNetworkAuthenticationRequired)
|
||||||
|
|
||||||
|
// see https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/how-to-use-vm-token#retry-guidance
|
||||||
|
|
||||||
|
const maxDelay time.Duration = 60 * time.Second
|
||||||
|
|
||||||
|
attempt := 0
|
||||||
|
delay := time.Duration(0)
|
||||||
|
|
||||||
|
for attempt < maxAttempts {
|
||||||
|
resp, err = sender.Do(req)
|
||||||
|
// retry on temporary network errors, e.g. transient network failures.
|
||||||
|
// if we don't receive a response then assume we can't connect to the
|
||||||
|
// endpoint so we're likely not running on an Azure VM so don't retry.
|
||||||
|
if (err != nil && !isTemporaryNetworkError(err)) || resp == nil || resp.StatusCode == http.StatusOK || !containsInt(retries, resp.StatusCode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform exponential backoff with a cap.
|
||||||
|
// must increment attempt before calculating delay.
|
||||||
|
attempt++
|
||||||
|
// the base value of 2 is the "delta backoff" as specified in the guidance doc
|
||||||
|
delay += (time.Duration(math.Pow(2, float64(attempt))) * time.Second)
|
||||||
|
if delay > maxDelay {
|
||||||
|
delay = maxDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(delay):
|
||||||
|
// intentionally left blank
|
||||||
|
case <-req.Context().Done():
|
||||||
|
err = req.Context().Err()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if the specified error is a temporary network error or false if it's not.
|
||||||
|
// if the error doesn't implement the net.Error interface the return value is true.
|
||||||
|
func isTemporaryNetworkError(err error) bool {
|
||||||
|
if netErr, ok := err.(net.Error); !ok || (ok && netErr.Temporary()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if slice ints contains the value n
|
||||||
|
func containsInt(ints []int, n int) bool {
|
||||||
|
for _, i := range ints {
|
||||||
|
if i == n {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoRefresh enables or disables automatic refreshing of stale tokens.
|
||||||
|
func (spt *ServicePrincipalToken) SetAutoRefresh(autoRefresh bool) {
|
||||||
|
spt.inner.AutoRefresh = autoRefresh
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRefreshWithin sets the interval within which if the token will expire, EnsureFresh will
|
||||||
|
// refresh the token.
|
||||||
|
func (spt *ServicePrincipalToken) SetRefreshWithin(d time.Duration) {
|
||||||
|
spt.inner.RefreshWithin = d
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSender sets the http.Client used when obtaining the Service Principal token. An
|
||||||
|
// undecorated http.Client is used by default.
|
||||||
|
func (spt *ServicePrincipalToken) SetSender(s Sender) { spt.sender = s }
|
||||||
|
|
||||||
|
// OAuthToken implements the OAuthTokenProvider interface. It returns the current access token.
|
||||||
|
func (spt *ServicePrincipalToken) OAuthToken() string {
|
||||||
|
spt.refreshLock.RLock()
|
||||||
|
defer spt.refreshLock.RUnlock()
|
||||||
|
return spt.inner.Token.OAuthToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token returns a copy of the current token.
|
||||||
|
func (spt *ServicePrincipalToken) Token() Token {
|
||||||
|
spt.refreshLock.RLock()
|
||||||
|
defer spt.refreshLock.RUnlock()
|
||||||
|
return spt.inner.Token
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Azure/go-autorest/autorest/adal"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bearerChallengeHeader = "Www-Authenticate"
|
||||||
|
bearer = "Bearer"
|
||||||
|
tenantID = "tenantID"
|
||||||
|
apiKeyAuthorizerHeader = "Ocp-Apim-Subscription-Key"
|
||||||
|
bingAPISdkHeader = "X-BingApis-SDK-Client"
|
||||||
|
golangBingAPISdkHeaderValue = "Go-SDK"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authorizer is the interface that provides a PrepareDecorator used to supply request
|
||||||
|
// authorization. Most often, the Authorizer decorator runs last so it has access to the full
|
||||||
|
// state of the formed HTTP request.
|
||||||
|
type Authorizer interface {
|
||||||
|
WithAuthorization() PrepareDecorator
|
||||||
|
}
|
||||||
|
|
||||||
|
// NullAuthorizer implements a default, "do nothing" Authorizer.
|
||||||
|
type NullAuthorizer struct{}
|
||||||
|
|
||||||
|
// WithAuthorization returns a PrepareDecorator that does nothing.
|
||||||
|
func (na NullAuthorizer) WithAuthorization() PrepareDecorator {
|
||||||
|
return WithNothing()
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIKeyAuthorizer implements API Key authorization.
|
||||||
|
type APIKeyAuthorizer struct {
|
||||||
|
headers map[string]interface{}
|
||||||
|
queryParameters map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIKeyAuthorizerWithHeaders creates an ApiKeyAuthorizer with headers.
|
||||||
|
func NewAPIKeyAuthorizerWithHeaders(headers map[string]interface{}) *APIKeyAuthorizer {
|
||||||
|
return NewAPIKeyAuthorizer(headers, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIKeyAuthorizerWithQueryParameters creates an ApiKeyAuthorizer with query parameters.
|
||||||
|
func NewAPIKeyAuthorizerWithQueryParameters(queryParameters map[string]interface{}) *APIKeyAuthorizer {
|
||||||
|
return NewAPIKeyAuthorizer(nil, queryParameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAPIKeyAuthorizer creates an ApiKeyAuthorizer with headers.
|
||||||
|
func NewAPIKeyAuthorizer(headers map[string]interface{}, queryParameters map[string]interface{}) *APIKeyAuthorizer {
|
||||||
|
return &APIKeyAuthorizer{headers: headers, queryParameters: queryParameters}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthorization returns a PrepareDecorator that adds an HTTP headers and Query Parameters
|
||||||
|
func (aka *APIKeyAuthorizer) WithAuthorization() PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return DecoratePreparer(p, WithHeaders(aka.headers), WithQueryParameters(aka.queryParameters))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CognitiveServicesAuthorizer implements authorization for Cognitive Services.
|
||||||
|
type CognitiveServicesAuthorizer struct {
|
||||||
|
subscriptionKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCognitiveServicesAuthorizer is
|
||||||
|
func NewCognitiveServicesAuthorizer(subscriptionKey string) *CognitiveServicesAuthorizer {
|
||||||
|
return &CognitiveServicesAuthorizer{subscriptionKey: subscriptionKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthorization is
|
||||||
|
func (csa *CognitiveServicesAuthorizer) WithAuthorization() PrepareDecorator {
|
||||||
|
headers := make(map[string]interface{})
|
||||||
|
headers[apiKeyAuthorizerHeader] = csa.subscriptionKey
|
||||||
|
headers[bingAPISdkHeader] = golangBingAPISdkHeaderValue
|
||||||
|
|
||||||
|
return NewAPIKeyAuthorizerWithHeaders(headers).WithAuthorization()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BearerAuthorizer implements the bearer authorization
|
||||||
|
type BearerAuthorizer struct {
|
||||||
|
tokenProvider adal.OAuthTokenProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBearerAuthorizer crates a BearerAuthorizer using the given token provider
|
||||||
|
func NewBearerAuthorizer(tp adal.OAuthTokenProvider) *BearerAuthorizer {
|
||||||
|
return &BearerAuthorizer{tokenProvider: tp}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
|
||||||
|
// value is "Bearer " followed by the token.
|
||||||
|
//
|
||||||
|
// By default, the token will be automatically refreshed through the Refresher interface.
|
||||||
|
func (ba *BearerAuthorizer) WithAuthorization() PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
// the ordering is important here, prefer RefresherWithContext if available
|
||||||
|
if refresher, ok := ba.tokenProvider.(adal.RefresherWithContext); ok {
|
||||||
|
err = refresher.EnsureFreshWithContext(r.Context())
|
||||||
|
} else if refresher, ok := ba.tokenProvider.(adal.Refresher); ok {
|
||||||
|
err = refresher.EnsureFresh()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
var resp *http.Response
|
||||||
|
if tokError, ok := err.(adal.TokenRefreshError); ok {
|
||||||
|
resp = tokError.Response()
|
||||||
|
}
|
||||||
|
return r, NewErrorWithError(err, "azure.BearerAuthorizer", "WithAuthorization", resp,
|
||||||
|
"Failed to refresh the Token for request to %s", r.URL)
|
||||||
|
}
|
||||||
|
return Prepare(r, WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", ba.tokenProvider.OAuthToken())))
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BearerAuthorizerCallbackFunc is the authentication callback signature.
|
||||||
|
type BearerAuthorizerCallbackFunc func(tenantID, resource string) (*BearerAuthorizer, error)
|
||||||
|
|
||||||
|
// BearerAuthorizerCallback implements bearer authorization via a callback.
|
||||||
|
type BearerAuthorizerCallback struct {
|
||||||
|
sender Sender
|
||||||
|
callback BearerAuthorizerCallbackFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBearerAuthorizerCallback creates a bearer authorization callback. The callback
|
||||||
|
// is invoked when the HTTP request is submitted.
|
||||||
|
func NewBearerAuthorizerCallback(sender Sender, callback BearerAuthorizerCallbackFunc) *BearerAuthorizerCallback {
|
||||||
|
if sender == nil {
|
||||||
|
sender = &http.Client{}
|
||||||
|
}
|
||||||
|
return &BearerAuthorizerCallback{sender: sender, callback: callback}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose value
|
||||||
|
// is "Bearer " followed by the token. The BearerAuthorizer is obtained via a user-supplied callback.
|
||||||
|
//
|
||||||
|
// By default, the token will be automatically refreshed through the Refresher interface.
|
||||||
|
func (bacb *BearerAuthorizerCallback) WithAuthorization() PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
// make a copy of the request and remove the body as it's not
|
||||||
|
// required and avoids us having to create a copy of it.
|
||||||
|
rCopy := *r
|
||||||
|
removeRequestBody(&rCopy)
|
||||||
|
|
||||||
|
resp, err := bacb.sender.Do(&rCopy)
|
||||||
|
if err == nil && resp.StatusCode == 401 {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if hasBearerChallenge(resp) {
|
||||||
|
bc, err := newBearerChallenge(resp)
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
if bacb.callback != nil {
|
||||||
|
ba, err := bacb.callback(bc.values[tenantID], bc.values["resource"])
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
return Prepare(r, ba.WithAuthorization())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if the HTTP response contains a bearer challenge
|
||||||
|
func hasBearerChallenge(resp *http.Response) bool {
|
||||||
|
authHeader := resp.Header.Get(bearerChallengeHeader)
|
||||||
|
if len(authHeader) == 0 || strings.Index(authHeader, bearer) < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type bearerChallenge struct {
|
||||||
|
values map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBearerChallenge(resp *http.Response) (bc bearerChallenge, err error) {
|
||||||
|
challenge := strings.TrimSpace(resp.Header.Get(bearerChallengeHeader))
|
||||||
|
trimmedChallenge := challenge[len(bearer)+1:]
|
||||||
|
|
||||||
|
// challenge is a set of key=value pairs that are comma delimited
|
||||||
|
pairs := strings.Split(trimmedChallenge, ",")
|
||||||
|
if len(pairs) < 1 {
|
||||||
|
err = fmt.Errorf("challenge '%s' contains no pairs", challenge)
|
||||||
|
return bc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bc.values = make(map[string]string)
|
||||||
|
for i := range pairs {
|
||||||
|
trimmedPair := strings.TrimSpace(pairs[i])
|
||||||
|
pair := strings.Split(trimmedPair, "=")
|
||||||
|
if len(pair) == 2 {
|
||||||
|
// remove the enclosing quotes
|
||||||
|
key := strings.Trim(pair[0], "\"")
|
||||||
|
value := strings.Trim(pair[1], "\"")
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case "authorization", "authorization_uri":
|
||||||
|
// strip the tenant ID from the authorization URL
|
||||||
|
asURL, err := url.Parse(value)
|
||||||
|
if err != nil {
|
||||||
|
return bc, err
|
||||||
|
}
|
||||||
|
bc.values[tenantID] = asURL.Path[1:]
|
||||||
|
default:
|
||||||
|
bc.values[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bc, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventGridKeyAuthorizer implements authorization for event grid using key authentication.
|
||||||
|
type EventGridKeyAuthorizer struct {
|
||||||
|
topicKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventGridKeyAuthorizer creates a new EventGridKeyAuthorizer
|
||||||
|
// with the specified topic key.
|
||||||
|
func NewEventGridKeyAuthorizer(topicKey string) EventGridKeyAuthorizer {
|
||||||
|
return EventGridKeyAuthorizer{topicKey: topicKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthorization returns a PrepareDecorator that adds the aeg-sas-key authentication header.
|
||||||
|
func (egta EventGridKeyAuthorizer) WithAuthorization() PrepareDecorator {
|
||||||
|
headers := map[string]interface{}{
|
||||||
|
"aeg-sas-key": egta.topicKey,
|
||||||
|
}
|
||||||
|
return NewAPIKeyAuthorizerWithHeaders(headers).WithAuthorization()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
Package autorest implements an HTTP request pipeline suitable for use across multiple go-routines
|
||||||
|
and provides the shared routines relied on by AutoRest (see https://github.com/Azure/autorest/)
|
||||||
|
generated Go code.
|
||||||
|
|
||||||
|
The package breaks sending and responding to HTTP requests into three phases: Preparing, Sending,
|
||||||
|
and Responding. A typical pattern is:
|
||||||
|
|
||||||
|
req, err := Prepare(&http.Request{},
|
||||||
|
token.WithAuthorization())
|
||||||
|
|
||||||
|
resp, err := Send(req,
|
||||||
|
WithLogging(logger),
|
||||||
|
DoErrorIfStatusCode(http.StatusInternalServerError),
|
||||||
|
DoCloseIfError(),
|
||||||
|
DoRetryForAttempts(5, time.Second))
|
||||||
|
|
||||||
|
err = Respond(resp,
|
||||||
|
ByDiscardingBody(),
|
||||||
|
ByClosing())
|
||||||
|
|
||||||
|
Each phase relies on decorators to modify and / or manage processing. Decorators may first modify
|
||||||
|
and then pass the data along, pass the data first and then modify the result, or wrap themselves
|
||||||
|
around passing the data (such as a logger might do). Decorators run in the order provided. For
|
||||||
|
example, the following:
|
||||||
|
|
||||||
|
req, err := Prepare(&http.Request{},
|
||||||
|
WithBaseURL("https://microsoft.com/"),
|
||||||
|
WithPath("a"),
|
||||||
|
WithPath("b"),
|
||||||
|
WithPath("c"))
|
||||||
|
|
||||||
|
will set the URL to:
|
||||||
|
|
||||||
|
https://microsoft.com/a/b/c
|
||||||
|
|
||||||
|
Preparers and Responders may be shared and re-used (assuming the underlying decorators support
|
||||||
|
sharing and re-use). Performant use is obtained by creating one or more Preparers and Responders
|
||||||
|
shared among multiple go-routines, and a single Sender shared among multiple sending go-routines,
|
||||||
|
all bound together by means of input / output channels.
|
||||||
|
|
||||||
|
Decorators hold their passed state within a closure (such as the path components in the example
|
||||||
|
above). Be careful to share Preparers and Responders only in a context where such held state
|
||||||
|
applies. For example, it may not make sense to share a Preparer that applies a query string from a
|
||||||
|
fixed set of values. Similarly, sharing a Responder that reads the response body into a passed
|
||||||
|
struct (e.g., ByUnmarshallingJson) is likely incorrect.
|
||||||
|
|
||||||
|
Lastly, the Swagger specification (https://swagger.io) that drives AutoRest
|
||||||
|
(https://github.com/Azure/autorest/) precisely defines two date forms: date and date-time. The
|
||||||
|
github.com/Azure/go-autorest/autorest/date package provides time.Time derivations to ensure
|
||||||
|
correct parsing and formatting.
|
||||||
|
|
||||||
|
Errors raised by autorest objects and methods will conform to the autorest.Error interface.
|
||||||
|
|
||||||
|
See the included examples for more detail. For details on the suggested use of this package by
|
||||||
|
generated clients, see the Client described below.
|
||||||
|
*/
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HeaderLocation specifies the HTTP Location header.
|
||||||
|
HeaderLocation = "Location"
|
||||||
|
|
||||||
|
// HeaderRetryAfter specifies the HTTP Retry-After header.
|
||||||
|
HeaderRetryAfter = "Retry-After"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResponseHasStatusCode returns true if the status code in the HTTP Response is in the passed set
|
||||||
|
// and false otherwise.
|
||||||
|
func ResponseHasStatusCode(resp *http.Response, codes ...int) bool {
|
||||||
|
if resp == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return containsInt(codes, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocation retrieves the URL from the Location header of the passed response.
|
||||||
|
func GetLocation(resp *http.Response) string {
|
||||||
|
return resp.Header.Get(HeaderLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRetryAfter extracts the retry delay from the Retry-After header of the passed response. If
|
||||||
|
// the header is absent or is malformed, it will return the supplied default delay time.Duration.
|
||||||
|
func GetRetryAfter(resp *http.Response, defaultDelay time.Duration) time.Duration {
|
||||||
|
retry := resp.Header.Get(HeaderRetryAfter)
|
||||||
|
if retry == "" {
|
||||||
|
return defaultDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := time.ParseDuration(retry + "s")
|
||||||
|
if err != nil {
|
||||||
|
return defaultDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPollingRequest allocates and returns a new http.Request to poll for the passed response.
|
||||||
|
func NewPollingRequest(resp *http.Response, cancel <-chan struct{}) (*http.Request, error) {
|
||||||
|
location := GetLocation(resp)
|
||||||
|
if location == "" {
|
||||||
|
return nil, NewErrorWithResponse("autorest", "NewPollingRequest", resp, "Location header missing from response that requires polling")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := Prepare(&http.Request{Cancel: cancel},
|
||||||
|
AsGet(),
|
||||||
|
WithBaseURL(location))
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorWithError(err, "autorest", "NewPollingRequest", nil, "Failure creating poll request to %s", location)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPollingRequestWithContext allocates and returns a new http.Request with the specified context to poll for the passed response.
|
||||||
|
func NewPollingRequestWithContext(ctx context.Context, resp *http.Response) (*http.Request, error) {
|
||||||
|
location := GetLocation(resp)
|
||||||
|
if location == "" {
|
||||||
|
return nil, NewErrorWithResponse("autorest", "NewPollingRequestWithContext", resp, "Location header missing from response that requires polling")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := Prepare((&http.Request{}).WithContext(ctx),
|
||||||
|
AsGet(),
|
||||||
|
WithBaseURL(location))
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorWithError(err, "autorest", "NewPollingRequestWithContext", nil, "Failure creating poll request to %s", location)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,916 @@
|
||||||
|
package azure
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
headerAsyncOperation = "Azure-AsyncOperation"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
operationInProgress string = "InProgress"
|
||||||
|
operationCanceled string = "Canceled"
|
||||||
|
operationFailed string = "Failed"
|
||||||
|
operationSucceeded string = "Succeeded"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pollingCodes = [...]int{http.StatusNoContent, http.StatusAccepted, http.StatusCreated, http.StatusOK}
|
||||||
|
|
||||||
|
// Future provides a mechanism to access the status and results of an asynchronous request.
|
||||||
|
// Since futures are stateful they should be passed by value to avoid race conditions.
|
||||||
|
type Future struct {
|
||||||
|
req *http.Request // legacy
|
||||||
|
pt pollingTracker
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFuture returns a new Future object initialized with the specified request.
|
||||||
|
// Deprecated: Please use NewFutureFromResponse instead.
|
||||||
|
func NewFuture(req *http.Request) Future {
|
||||||
|
return Future{req: req}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFutureFromResponse returns a new Future object initialized
|
||||||
|
// with the initial response from an asynchronous operation.
|
||||||
|
func NewFutureFromResponse(resp *http.Response) (Future, error) {
|
||||||
|
pt, err := createPollingTracker(resp)
|
||||||
|
if err != nil {
|
||||||
|
return Future{}, err
|
||||||
|
}
|
||||||
|
return Future{pt: pt}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response returns the last HTTP response.
|
||||||
|
func (f Future) Response() *http.Response {
|
||||||
|
if f.pt == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return f.pt.latestResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status returns the last status message of the operation.
|
||||||
|
func (f Future) Status() string {
|
||||||
|
if f.pt == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return f.pt.pollingStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollingMethod returns the method used to monitor the status of the asynchronous operation.
|
||||||
|
func (f Future) PollingMethod() PollingMethodType {
|
||||||
|
if f.pt == nil {
|
||||||
|
return PollingUnknown
|
||||||
|
}
|
||||||
|
return f.pt.pollingMethod()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done queries the service to see if the operation has completed.
|
||||||
|
func (f *Future) Done(sender autorest.Sender) (bool, error) {
|
||||||
|
// support for legacy Future implementation
|
||||||
|
if f.req != nil {
|
||||||
|
resp, err := sender.Do(f.req)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
pt, err := createPollingTracker(resp)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
f.pt = pt
|
||||||
|
f.req = nil
|
||||||
|
}
|
||||||
|
// end legacy
|
||||||
|
if f.pt == nil {
|
||||||
|
return false, autorest.NewError("Future", "Done", "future is not initialized")
|
||||||
|
}
|
||||||
|
if f.pt.hasTerminated() {
|
||||||
|
return true, f.pt.pollingError()
|
||||||
|
}
|
||||||
|
if err := f.pt.pollForStatus(sender); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := f.pt.checkForErrors(); err != nil {
|
||||||
|
return f.pt.hasTerminated(), err
|
||||||
|
}
|
||||||
|
if err := f.pt.updatePollingState(f.pt.provisioningStateApplicable()); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := f.pt.updateHeaders(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return f.pt.hasTerminated(), f.pt.pollingError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPollingDelay returns a duration the application should wait before checking
|
||||||
|
// the status of the asynchronous request and true; this value is returned from
|
||||||
|
// the service via the Retry-After response header. If the header wasn't returned
|
||||||
|
// then the function returns the zero-value time.Duration and false.
|
||||||
|
func (f Future) GetPollingDelay() (time.Duration, bool) {
|
||||||
|
if f.pt == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
resp := f.pt.latestResponse()
|
||||||
|
if resp == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
retry := resp.Header.Get(autorest.HeaderRetryAfter)
|
||||||
|
if retry == "" {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := time.ParseDuration(retry + "s")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return d, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForCompletion will return when one of the following conditions is met: the long
|
||||||
|
// running operation has completed, the provided context is cancelled, or the client's
|
||||||
|
// polling duration has been exceeded. It will retry failed polling attempts based on
|
||||||
|
// the retry value defined in the client up to the maximum retry attempts.
|
||||||
|
// Deprecated: Please use WaitForCompletionRef() instead.
|
||||||
|
func (f Future) WaitForCompletion(ctx context.Context, client autorest.Client) error {
|
||||||
|
return f.WaitForCompletionRef(ctx, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForCompletionRef will return when one of the following conditions is met: the long
|
||||||
|
// running operation has completed, the provided context is cancelled, or the client's
|
||||||
|
// polling duration has been exceeded. It will retry failed polling attempts based on
|
||||||
|
// the retry value defined in the client up to the maximum retry attempts.
|
||||||
|
func (f *Future) WaitForCompletionRef(ctx context.Context, client autorest.Client) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, client.PollingDuration)
|
||||||
|
defer cancel()
|
||||||
|
done, err := f.Done(client)
|
||||||
|
for attempts := 0; !done; done, err = f.Done(client) {
|
||||||
|
if attempts >= client.RetryAttempts {
|
||||||
|
return autorest.NewErrorWithError(err, "Future", "WaitForCompletion", f.pt.latestResponse(), "the number of retries has been exceeded")
|
||||||
|
}
|
||||||
|
// we want delayAttempt to be zero in the non-error case so
|
||||||
|
// that DelayForBackoff doesn't perform exponential back-off
|
||||||
|
var delayAttempt int
|
||||||
|
var delay time.Duration
|
||||||
|
if err == nil {
|
||||||
|
// check for Retry-After delay, if not present use the client's polling delay
|
||||||
|
var ok bool
|
||||||
|
delay, ok = f.GetPollingDelay()
|
||||||
|
if !ok {
|
||||||
|
delay = client.PollingDelay
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// there was an error polling for status so perform exponential
|
||||||
|
// back-off based on the number of attempts using the client's retry
|
||||||
|
// duration. update attempts after delayAttempt to avoid off-by-one.
|
||||||
|
delayAttempt = attempts
|
||||||
|
delay = client.RetryDuration
|
||||||
|
attempts++
|
||||||
|
}
|
||||||
|
// wait until the delay elapses or the context is cancelled
|
||||||
|
delayElapsed := autorest.DelayForBackoff(delay, delayAttempt, ctx.Done())
|
||||||
|
if !delayElapsed {
|
||||||
|
return autorest.NewErrorWithError(ctx.Err(), "Future", "WaitForCompletion", f.pt.latestResponse(), "context has been cancelled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface.
|
||||||
|
func (f Future) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(f.pt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||||
|
func (f *Future) UnmarshalJSON(data []byte) error {
|
||||||
|
// unmarshal into JSON object to determine the tracker type
|
||||||
|
obj := map[string]interface{}{}
|
||||||
|
err := json.Unmarshal(data, &obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if obj["method"] == nil {
|
||||||
|
return autorest.NewError("Future", "UnmarshalJSON", "missing 'method' property")
|
||||||
|
}
|
||||||
|
method := obj["method"].(string)
|
||||||
|
switch strings.ToUpper(method) {
|
||||||
|
case http.MethodDelete:
|
||||||
|
f.pt = &pollingTrackerDelete{}
|
||||||
|
case http.MethodPatch:
|
||||||
|
f.pt = &pollingTrackerPatch{}
|
||||||
|
case http.MethodPost:
|
||||||
|
f.pt = &pollingTrackerPost{}
|
||||||
|
case http.MethodPut:
|
||||||
|
f.pt = &pollingTrackerPut{}
|
||||||
|
default:
|
||||||
|
return autorest.NewError("Future", "UnmarshalJSON", "unsupoorted method '%s'", method)
|
||||||
|
}
|
||||||
|
// now unmarshal into the tracker
|
||||||
|
return json.Unmarshal(data, &f.pt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollingURL returns the URL used for retrieving the status of the long-running operation.
|
||||||
|
func (f Future) PollingURL() string {
|
||||||
|
if f.pt == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return f.pt.pollingURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResult should be called once polling has completed successfully.
|
||||||
|
// It makes the final GET call to retrieve the resultant payload.
|
||||||
|
func (f Future) GetResult(sender autorest.Sender) (*http.Response, error) {
|
||||||
|
if f.pt.finalGetURL() == "" {
|
||||||
|
// we can end up in this situation if the async operation returns a 200
|
||||||
|
// with no polling URLs. in that case return the response which should
|
||||||
|
// contain the JSON payload (only do this for successful terminal cases).
|
||||||
|
if lr := f.pt.latestResponse(); lr != nil && f.pt.hasSucceeded() {
|
||||||
|
return lr, nil
|
||||||
|
}
|
||||||
|
return nil, autorest.NewError("Future", "GetResult", "missing URL for retrieving result")
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, f.pt.finalGetURL(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return sender.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pollingTracker interface {
|
||||||
|
// these methods can differ per tracker
|
||||||
|
|
||||||
|
// checks the response headers and status code to determine the polling mechanism
|
||||||
|
updateHeaders() error
|
||||||
|
|
||||||
|
// checks the response for tracker-specific error conditions
|
||||||
|
checkForErrors() error
|
||||||
|
|
||||||
|
// returns true if provisioning state should be checked
|
||||||
|
provisioningStateApplicable() bool
|
||||||
|
|
||||||
|
// methods common to all trackers
|
||||||
|
|
||||||
|
// initializes the tracker's internal state, call this when the tracker is created
|
||||||
|
initializeState() error
|
||||||
|
|
||||||
|
// makes an HTTP request to check the status of the LRO
|
||||||
|
pollForStatus(sender autorest.Sender) error
|
||||||
|
|
||||||
|
// updates internal tracker state, call this after each call to pollForStatus
|
||||||
|
updatePollingState(provStateApl bool) error
|
||||||
|
|
||||||
|
// returns the error response from the service, can be nil
|
||||||
|
pollingError() error
|
||||||
|
|
||||||
|
// returns the polling method being used
|
||||||
|
pollingMethod() PollingMethodType
|
||||||
|
|
||||||
|
// returns the state of the LRO as returned from the service
|
||||||
|
pollingStatus() string
|
||||||
|
|
||||||
|
// returns the URL used for polling status
|
||||||
|
pollingURL() string
|
||||||
|
|
||||||
|
// returns the URL used for the final GET to retrieve the resource
|
||||||
|
finalGetURL() string
|
||||||
|
|
||||||
|
// returns true if the LRO is in a terminal state
|
||||||
|
hasTerminated() bool
|
||||||
|
|
||||||
|
// returns true if the LRO is in a failed terminal state
|
||||||
|
hasFailed() bool
|
||||||
|
|
||||||
|
// returns true if the LRO is in a successful terminal state
|
||||||
|
hasSucceeded() bool
|
||||||
|
|
||||||
|
// returns the cached HTTP response after a call to pollForStatus(), can be nil
|
||||||
|
latestResponse() *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
type pollingTrackerBase struct {
|
||||||
|
// resp is the last response, either from the submission of the LRO or from polling
|
||||||
|
resp *http.Response
|
||||||
|
|
||||||
|
// method is the HTTP verb, this is needed for deserialization
|
||||||
|
Method string `json:"method"`
|
||||||
|
|
||||||
|
// rawBody is the raw JSON response body
|
||||||
|
rawBody map[string]interface{}
|
||||||
|
|
||||||
|
// denotes if polling is using async-operation or location header
|
||||||
|
Pm PollingMethodType `json:"pollingMethod"`
|
||||||
|
|
||||||
|
// the URL to poll for status
|
||||||
|
URI string `json:"pollingURI"`
|
||||||
|
|
||||||
|
// the state of the LRO as returned from the service
|
||||||
|
State string `json:"lroState"`
|
||||||
|
|
||||||
|
// the URL to GET for the final result
|
||||||
|
FinalGetURI string `json:"resultURI"`
|
||||||
|
|
||||||
|
// used to hold an error object returned from the service
|
||||||
|
Err *ServiceError `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *pollingTrackerBase) initializeState() error {
|
||||||
|
// determine the initial polling state based on response body and/or HTTP status
|
||||||
|
// code. this is applicable to the initial LRO response, not polling responses!
|
||||||
|
pt.Method = pt.resp.Request.Method
|
||||||
|
if err := pt.updateRawBody(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch pt.resp.StatusCode {
|
||||||
|
case http.StatusOK:
|
||||||
|
if ps := pt.getProvisioningState(); ps != nil {
|
||||||
|
pt.State = *ps
|
||||||
|
} else {
|
||||||
|
pt.State = operationSucceeded
|
||||||
|
}
|
||||||
|
case http.StatusCreated:
|
||||||
|
if ps := pt.getProvisioningState(); ps != nil {
|
||||||
|
pt.State = *ps
|
||||||
|
} else {
|
||||||
|
pt.State = operationInProgress
|
||||||
|
}
|
||||||
|
case http.StatusAccepted:
|
||||||
|
pt.State = operationInProgress
|
||||||
|
case http.StatusNoContent:
|
||||||
|
pt.State = operationSucceeded
|
||||||
|
default:
|
||||||
|
pt.State = operationFailed
|
||||||
|
pt.updateErrorFromResponse()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) getProvisioningState() *string {
|
||||||
|
if pt.rawBody != nil && pt.rawBody["properties"] != nil {
|
||||||
|
p := pt.rawBody["properties"].(map[string]interface{})
|
||||||
|
if ps := p["provisioningState"]; ps != nil {
|
||||||
|
s := ps.(string)
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *pollingTrackerBase) updateRawBody() error {
|
||||||
|
pt.rawBody = map[string]interface{}{}
|
||||||
|
if pt.resp.ContentLength != 0 {
|
||||||
|
defer pt.resp.Body.Close()
|
||||||
|
b, err := ioutil.ReadAll(pt.resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to read response body")
|
||||||
|
}
|
||||||
|
// put the body back so it's available to other callers
|
||||||
|
pt.resp.Body = ioutil.NopCloser(bytes.NewReader(b))
|
||||||
|
if err = json.Unmarshal(b, &pt.rawBody); err != nil {
|
||||||
|
return autorest.NewErrorWithError(err, "pollingTrackerBase", "updateRawBody", nil, "failed to unmarshal response body")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *pollingTrackerBase) pollForStatus(sender autorest.Sender) error {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, pt.URI, nil)
|
||||||
|
if err != nil {
|
||||||
|
return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed to create HTTP request")
|
||||||
|
}
|
||||||
|
// attach the context from the original request if available (it will be absent for deserialized futures)
|
||||||
|
if pt.resp != nil {
|
||||||
|
req = req.WithContext(pt.resp.Request.Context())
|
||||||
|
}
|
||||||
|
pt.resp, err = sender.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return autorest.NewErrorWithError(err, "pollingTrackerBase", "pollForStatus", nil, "failed to send HTTP request")
|
||||||
|
}
|
||||||
|
if autorest.ResponseHasStatusCode(pt.resp, pollingCodes[:]...) {
|
||||||
|
// reset the service error on success case
|
||||||
|
pt.Err = nil
|
||||||
|
err = pt.updateRawBody()
|
||||||
|
} else {
|
||||||
|
// check response body for error content
|
||||||
|
pt.updateErrorFromResponse()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempts to unmarshal a ServiceError type from the response body.
|
||||||
|
// if that fails then make a best attempt at creating something meaningful.
|
||||||
|
func (pt *pollingTrackerBase) updateErrorFromResponse() {
|
||||||
|
var err error
|
||||||
|
if pt.resp.ContentLength != 0 {
|
||||||
|
type respErr struct {
|
||||||
|
ServiceError *ServiceError `json:"error"`
|
||||||
|
}
|
||||||
|
re := respErr{}
|
||||||
|
defer pt.resp.Body.Close()
|
||||||
|
var b []byte
|
||||||
|
b, err = ioutil.ReadAll(pt.resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
goto Default
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal(b, &re); err != nil {
|
||||||
|
goto Default
|
||||||
|
}
|
||||||
|
// unmarshalling the error didn't yield anything, try unwrapped error
|
||||||
|
if re.ServiceError == nil {
|
||||||
|
err = json.Unmarshal(b, &re.ServiceError)
|
||||||
|
if err != nil {
|
||||||
|
goto Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if re.ServiceError != nil {
|
||||||
|
pt.Err = re.ServiceError
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Default:
|
||||||
|
se := &ServiceError{
|
||||||
|
Code: fmt.Sprintf("HTTP status code %v", pt.resp.StatusCode),
|
||||||
|
Message: pt.resp.Status,
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
se.InnerError = make(map[string]interface{})
|
||||||
|
se.InnerError["unmarshalError"] = err.Error()
|
||||||
|
}
|
||||||
|
pt.Err = se
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *pollingTrackerBase) updatePollingState(provStateApl bool) error {
|
||||||
|
if pt.Pm == PollingAsyncOperation && pt.rawBody["status"] != nil {
|
||||||
|
pt.State = pt.rawBody["status"].(string)
|
||||||
|
} else {
|
||||||
|
if pt.resp.StatusCode == http.StatusAccepted {
|
||||||
|
pt.State = operationInProgress
|
||||||
|
} else if provStateApl {
|
||||||
|
if ps := pt.getProvisioningState(); ps != nil {
|
||||||
|
pt.State = *ps
|
||||||
|
} else {
|
||||||
|
pt.State = operationSucceeded
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return autorest.NewError("pollingTrackerBase", "updatePollingState", "the response from the async operation has an invalid status code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if the operation has failed update the error state
|
||||||
|
if pt.hasFailed() {
|
||||||
|
pt.updateErrorFromResponse()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) pollingError() error {
|
||||||
|
if pt.Err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return pt.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) pollingMethod() PollingMethodType {
|
||||||
|
return pt.Pm
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) pollingStatus() string {
|
||||||
|
return pt.State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) pollingURL() string {
|
||||||
|
return pt.URI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) finalGetURL() string {
|
||||||
|
return pt.FinalGetURI
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) hasTerminated() bool {
|
||||||
|
return strings.EqualFold(pt.State, operationCanceled) || strings.EqualFold(pt.State, operationFailed) || strings.EqualFold(pt.State, operationSucceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) hasFailed() bool {
|
||||||
|
return strings.EqualFold(pt.State, operationCanceled) || strings.EqualFold(pt.State, operationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) hasSucceeded() bool {
|
||||||
|
return strings.EqualFold(pt.State, operationSucceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerBase) latestResponse() *http.Response {
|
||||||
|
return pt.resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// error checking common to all trackers
|
||||||
|
func (pt pollingTrackerBase) baseCheckForErrors() error {
|
||||||
|
// for Azure-AsyncOperations the response body cannot be nil or empty
|
||||||
|
if pt.Pm == PollingAsyncOperation {
|
||||||
|
if pt.resp.Body == nil || pt.resp.ContentLength == 0 {
|
||||||
|
return autorest.NewError("pollingTrackerBase", "baseCheckForErrors", "for Azure-AsyncOperation response body cannot be nil")
|
||||||
|
}
|
||||||
|
if pt.rawBody["status"] == nil {
|
||||||
|
return autorest.NewError("pollingTrackerBase", "baseCheckForErrors", "missing status property in Azure-AsyncOperation response body")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
|
||||||
|
type pollingTrackerDelete struct {
|
||||||
|
pollingTrackerBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *pollingTrackerDelete) updateHeaders() error {
|
||||||
|
// for 201 the Location header is required
|
||||||
|
if pt.resp.StatusCode == http.StatusCreated {
|
||||||
|
if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
|
||||||
|
return err
|
||||||
|
} else if lh == "" {
|
||||||
|
return autorest.NewError("pollingTrackerDelete", "updateHeaders", "missing Location header in 201 response")
|
||||||
|
} else {
|
||||||
|
pt.URI = lh
|
||||||
|
}
|
||||||
|
pt.Pm = PollingLocation
|
||||||
|
pt.FinalGetURI = pt.URI
|
||||||
|
}
|
||||||
|
// for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
|
||||||
|
if pt.resp.StatusCode == http.StatusAccepted {
|
||||||
|
ao, err := getURLFromAsyncOpHeader(pt.resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if ao != "" {
|
||||||
|
pt.URI = ao
|
||||||
|
pt.Pm = PollingAsyncOperation
|
||||||
|
}
|
||||||
|
// if the Location header is invalid and we already have a polling URL
|
||||||
|
// then we don't care if the Location header URL is malformed.
|
||||||
|
if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
|
||||||
|
return err
|
||||||
|
} else if lh != "" {
|
||||||
|
if ao == "" {
|
||||||
|
pt.URI = lh
|
||||||
|
pt.Pm = PollingLocation
|
||||||
|
}
|
||||||
|
// when both headers are returned we use the value in the Location header for the final GET
|
||||||
|
pt.FinalGetURI = lh
|
||||||
|
}
|
||||||
|
// make sure a polling URL was found
|
||||||
|
if pt.URI == "" {
|
||||||
|
return autorest.NewError("pollingTrackerPost", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerDelete) checkForErrors() error {
|
||||||
|
return pt.baseCheckForErrors()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerDelete) provisioningStateApplicable() bool {
|
||||||
|
return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusNoContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// PATCH
|
||||||
|
|
||||||
|
type pollingTrackerPatch struct {
|
||||||
|
pollingTrackerBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *pollingTrackerPatch) updateHeaders() error {
|
||||||
|
// by default we can use the original URL for polling and final GET
|
||||||
|
if pt.URI == "" {
|
||||||
|
pt.URI = pt.resp.Request.URL.String()
|
||||||
|
}
|
||||||
|
if pt.FinalGetURI == "" {
|
||||||
|
pt.FinalGetURI = pt.resp.Request.URL.String()
|
||||||
|
}
|
||||||
|
if pt.Pm == PollingUnknown {
|
||||||
|
pt.Pm = PollingRequestURI
|
||||||
|
}
|
||||||
|
// for 201 it's permissible for no headers to be returned
|
||||||
|
if pt.resp.StatusCode == http.StatusCreated {
|
||||||
|
if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil {
|
||||||
|
return err
|
||||||
|
} else if ao != "" {
|
||||||
|
pt.URI = ao
|
||||||
|
pt.Pm = PollingAsyncOperation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
|
||||||
|
// note the absence of the "final GET" mechanism for PATCH
|
||||||
|
if pt.resp.StatusCode == http.StatusAccepted {
|
||||||
|
ao, err := getURLFromAsyncOpHeader(pt.resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if ao != "" {
|
||||||
|
pt.URI = ao
|
||||||
|
pt.Pm = PollingAsyncOperation
|
||||||
|
}
|
||||||
|
if ao == "" {
|
||||||
|
if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
|
||||||
|
return err
|
||||||
|
} else if lh == "" {
|
||||||
|
return autorest.NewError("pollingTrackerPatch", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
|
||||||
|
} else {
|
||||||
|
pt.URI = lh
|
||||||
|
pt.Pm = PollingLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerPatch) checkForErrors() error {
|
||||||
|
return pt.baseCheckForErrors()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerPatch) provisioningStateApplicable() bool {
|
||||||
|
return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusCreated
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST
|
||||||
|
|
||||||
|
type pollingTrackerPost struct {
|
||||||
|
pollingTrackerBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *pollingTrackerPost) updateHeaders() error {
|
||||||
|
// 201 requires Location header
|
||||||
|
if pt.resp.StatusCode == http.StatusCreated {
|
||||||
|
if lh, err := getURLFromLocationHeader(pt.resp); err != nil {
|
||||||
|
return err
|
||||||
|
} else if lh == "" {
|
||||||
|
return autorest.NewError("pollingTrackerPost", "updateHeaders", "missing Location header in 201 response")
|
||||||
|
} else {
|
||||||
|
pt.URI = lh
|
||||||
|
pt.FinalGetURI = lh
|
||||||
|
pt.Pm = PollingLocation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
|
||||||
|
if pt.resp.StatusCode == http.StatusAccepted {
|
||||||
|
ao, err := getURLFromAsyncOpHeader(pt.resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if ao != "" {
|
||||||
|
pt.URI = ao
|
||||||
|
pt.Pm = PollingAsyncOperation
|
||||||
|
}
|
||||||
|
// if the Location header is invalid and we already have a polling URL
|
||||||
|
// then we don't care if the Location header URL is malformed.
|
||||||
|
if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
|
||||||
|
return err
|
||||||
|
} else if lh != "" {
|
||||||
|
if ao == "" {
|
||||||
|
pt.URI = lh
|
||||||
|
pt.Pm = PollingLocation
|
||||||
|
}
|
||||||
|
// when both headers are returned we use the value in the Location header for the final GET
|
||||||
|
pt.FinalGetURI = lh
|
||||||
|
}
|
||||||
|
// make sure a polling URL was found
|
||||||
|
if pt.URI == "" {
|
||||||
|
return autorest.NewError("pollingTrackerPost", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerPost) checkForErrors() error {
|
||||||
|
return pt.baseCheckForErrors()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerPost) provisioningStateApplicable() bool {
|
||||||
|
return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusNoContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT
|
||||||
|
|
||||||
|
type pollingTrackerPut struct {
|
||||||
|
pollingTrackerBase
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt *pollingTrackerPut) updateHeaders() error {
|
||||||
|
// by default we can use the original URL for polling and final GET
|
||||||
|
if pt.URI == "" {
|
||||||
|
pt.URI = pt.resp.Request.URL.String()
|
||||||
|
}
|
||||||
|
if pt.FinalGetURI == "" {
|
||||||
|
pt.FinalGetURI = pt.resp.Request.URL.String()
|
||||||
|
}
|
||||||
|
if pt.Pm == PollingUnknown {
|
||||||
|
pt.Pm = PollingRequestURI
|
||||||
|
}
|
||||||
|
// for 201 it's permissible for no headers to be returned
|
||||||
|
if pt.resp.StatusCode == http.StatusCreated {
|
||||||
|
if ao, err := getURLFromAsyncOpHeader(pt.resp); err != nil {
|
||||||
|
return err
|
||||||
|
} else if ao != "" {
|
||||||
|
pt.URI = ao
|
||||||
|
pt.Pm = PollingAsyncOperation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// for 202 prefer the Azure-AsyncOperation header but fall back to Location if necessary
|
||||||
|
if pt.resp.StatusCode == http.StatusAccepted {
|
||||||
|
ao, err := getURLFromAsyncOpHeader(pt.resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if ao != "" {
|
||||||
|
pt.URI = ao
|
||||||
|
pt.Pm = PollingAsyncOperation
|
||||||
|
}
|
||||||
|
// if the Location header is invalid and we already have a polling URL
|
||||||
|
// then we don't care if the Location header URL is malformed.
|
||||||
|
if lh, err := getURLFromLocationHeader(pt.resp); err != nil && pt.URI == "" {
|
||||||
|
return err
|
||||||
|
} else if lh != "" {
|
||||||
|
if ao == "" {
|
||||||
|
pt.URI = lh
|
||||||
|
pt.Pm = PollingLocation
|
||||||
|
}
|
||||||
|
// when both headers are returned we use the value in the Location header for the final GET
|
||||||
|
pt.FinalGetURI = lh
|
||||||
|
}
|
||||||
|
// make sure a polling URL was found
|
||||||
|
if pt.URI == "" {
|
||||||
|
return autorest.NewError("pollingTrackerPut", "updateHeaders", "didn't get any suitable polling URLs in 202 response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerPut) checkForErrors() error {
|
||||||
|
err := pt.baseCheckForErrors()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if there are no LRO headers then the body cannot be empty
|
||||||
|
ao, err := getURLFromAsyncOpHeader(pt.resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lh, err := getURLFromLocationHeader(pt.resp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ao == "" && lh == "" && len(pt.rawBody) == 0 {
|
||||||
|
return autorest.NewError("pollingTrackerPut", "checkForErrors", "the response did not contain a body")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pt pollingTrackerPut) provisioningStateApplicable() bool {
|
||||||
|
return pt.resp.StatusCode == http.StatusOK || pt.resp.StatusCode == http.StatusCreated
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a polling tracker based on the verb of the original request
|
||||||
|
func createPollingTracker(resp *http.Response) (pollingTracker, error) {
|
||||||
|
var pt pollingTracker
|
||||||
|
switch strings.ToUpper(resp.Request.Method) {
|
||||||
|
case http.MethodDelete:
|
||||||
|
pt = &pollingTrackerDelete{pollingTrackerBase: pollingTrackerBase{resp: resp}}
|
||||||
|
case http.MethodPatch:
|
||||||
|
pt = &pollingTrackerPatch{pollingTrackerBase: pollingTrackerBase{resp: resp}}
|
||||||
|
case http.MethodPost:
|
||||||
|
pt = &pollingTrackerPost{pollingTrackerBase: pollingTrackerBase{resp: resp}}
|
||||||
|
case http.MethodPut:
|
||||||
|
pt = &pollingTrackerPut{pollingTrackerBase: pollingTrackerBase{resp: resp}}
|
||||||
|
default:
|
||||||
|
return nil, autorest.NewError("azure", "createPollingTracker", "unsupported HTTP method %s", resp.Request.Method)
|
||||||
|
}
|
||||||
|
if err := pt.initializeState(); err != nil {
|
||||||
|
return pt, err
|
||||||
|
}
|
||||||
|
// this initializes the polling header values, we do this during creation in case the
|
||||||
|
// initial response send us invalid values; this way the API call will return a non-nil
|
||||||
|
// error (not doing this means the error shows up in Future.Done)
|
||||||
|
return pt, pt.updateHeaders()
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets the polling URL from the Azure-AsyncOperation header.
|
||||||
|
// ensures the URL is well-formed and absolute.
|
||||||
|
func getURLFromAsyncOpHeader(resp *http.Response) (string, error) {
|
||||||
|
s := resp.Header.Get(http.CanonicalHeaderKey(headerAsyncOperation))
|
||||||
|
if s == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if !isValidURL(s) {
|
||||||
|
return "", autorest.NewError("azure", "getURLFromAsyncOpHeader", "invalid polling URL '%s'", s)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets the polling URL from the Location header.
|
||||||
|
// ensures the URL is well-formed and absolute.
|
||||||
|
func getURLFromLocationHeader(resp *http.Response) (string, error) {
|
||||||
|
s := resp.Header.Get(http.CanonicalHeaderKey(autorest.HeaderLocation))
|
||||||
|
if s == "" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if !isValidURL(s) {
|
||||||
|
return "", autorest.NewError("azure", "getURLFromLocationHeader", "invalid polling URL '%s'", s)
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that the URL is valid and absolute
|
||||||
|
func isValidURL(s string) bool {
|
||||||
|
u, err := url.Parse(s)
|
||||||
|
return err == nil && u.IsAbs()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoPollForAsynchronous returns a SendDecorator that polls if the http.Response is for an Azure
|
||||||
|
// long-running operation. It will delay between requests for the duration specified in the
|
||||||
|
// RetryAfter header or, if the header is absent, the passed delay. Polling may be canceled via
|
||||||
|
// the context associated with the http.Request.
|
||||||
|
// Deprecated: Prefer using Futures to allow for non-blocking async operations.
|
||||||
|
func DoPollForAsynchronous(delay time.Duration) autorest.SendDecorator {
|
||||||
|
return func(s autorest.Sender) autorest.Sender {
|
||||||
|
return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
resp, err := s.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if !autorest.ResponseHasStatusCode(resp, pollingCodes[:]...) {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
future, err := NewFutureFromResponse(resp)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
// retry until either the LRO completes or we receive an error
|
||||||
|
var done bool
|
||||||
|
for done, err = future.Done(s); !done && err == nil; done, err = future.Done(s) {
|
||||||
|
// check for Retry-After delay, if not present use the specified polling delay
|
||||||
|
if pd, ok := future.GetPollingDelay(); ok {
|
||||||
|
delay = pd
|
||||||
|
}
|
||||||
|
// wait until the delay elapses or the context is cancelled
|
||||||
|
if delayElapsed := autorest.DelayForBackoff(delay, 0, r.Context().Done()); !delayElapsed {
|
||||||
|
return future.Response(),
|
||||||
|
autorest.NewErrorWithError(r.Context().Err(), "azure", "DoPollForAsynchronous", future.Response(), "context has been cancelled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return future.Response(), err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PollingMethodType defines a type used for enumerating polling mechanisms.
|
||||||
|
type PollingMethodType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PollingAsyncOperation indicates the polling method uses the Azure-AsyncOperation header.
|
||||||
|
PollingAsyncOperation PollingMethodType = "AsyncOperation"
|
||||||
|
|
||||||
|
// PollingLocation indicates the polling method uses the Location header.
|
||||||
|
PollingLocation PollingMethodType = "Location"
|
||||||
|
|
||||||
|
// PollingRequestURI indicates the polling method uses the original request URI.
|
||||||
|
PollingRequestURI PollingMethodType = "RequestURI"
|
||||||
|
|
||||||
|
// PollingUnknown indicates an unknown polling method and is the default value.
|
||||||
|
PollingUnknown PollingMethodType = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// AsyncOpIncompleteError is the type that's returned from a future that has not completed.
|
||||||
|
type AsyncOpIncompleteError struct {
|
||||||
|
// FutureType is the name of the type composed of a azure.Future.
|
||||||
|
FutureType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns an error message including the originating type name of the error.
|
||||||
|
func (e AsyncOpIncompleteError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: asynchronous operation has not completed", e.FutureType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAsyncOpIncompleteError creates a new AsyncOpIncompleteError with the specified parameters.
|
||||||
|
func NewAsyncOpIncompleteError(futureType string) AsyncOpIncompleteError {
|
||||||
|
return AsyncOpIncompleteError{
|
||||||
|
FutureType: futureType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
// Package azure provides Azure-specific implementations used with AutoRest.
|
||||||
|
// See the included examples for more detail.
|
||||||
|
package azure
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HeaderClientID is the Azure extension header to set a user-specified request ID.
|
||||||
|
HeaderClientID = "x-ms-client-request-id"
|
||||||
|
|
||||||
|
// HeaderReturnClientID is the Azure extension header to set if the user-specified request ID
|
||||||
|
// should be included in the response.
|
||||||
|
HeaderReturnClientID = "x-ms-return-client-request-id"
|
||||||
|
|
||||||
|
// HeaderRequestID is the Azure extension header of the service generated request ID returned
|
||||||
|
// in the response.
|
||||||
|
HeaderRequestID = "x-ms-request-id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceError encapsulates the error response from an Azure service.
|
||||||
|
// It adhears to the OData v4 specification for error responses.
|
||||||
|
type ServiceError struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Target *string `json:"target"`
|
||||||
|
Details []map[string]interface{} `json:"details"`
|
||||||
|
InnerError map[string]interface{} `json:"innererror"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (se ServiceError) Error() string {
|
||||||
|
result := fmt.Sprintf("Code=%q Message=%q", se.Code, se.Message)
|
||||||
|
|
||||||
|
if se.Target != nil {
|
||||||
|
result += fmt.Sprintf(" Target=%q", *se.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
if se.Details != nil {
|
||||||
|
d, err := json.Marshal(se.Details)
|
||||||
|
if err != nil {
|
||||||
|
result += fmt.Sprintf(" Details=%v", se.Details)
|
||||||
|
}
|
||||||
|
result += fmt.Sprintf(" Details=%v", string(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
if se.InnerError != nil {
|
||||||
|
d, err := json.Marshal(se.InnerError)
|
||||||
|
if err != nil {
|
||||||
|
result += fmt.Sprintf(" InnerError=%v", se.InnerError)
|
||||||
|
}
|
||||||
|
result += fmt.Sprintf(" InnerError=%v", string(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Unmarshaler interface for the ServiceError type.
|
||||||
|
func (se *ServiceError) UnmarshalJSON(b []byte) error {
|
||||||
|
// per the OData v4 spec the details field must be an array of JSON objects.
|
||||||
|
// unfortunately not all services adhear to the spec and just return a single
|
||||||
|
// object instead of an array with one object. so we have to perform some
|
||||||
|
// shenanigans to accommodate both cases.
|
||||||
|
// http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
|
||||||
|
|
||||||
|
type serviceError1 struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Target *string `json:"target"`
|
||||||
|
Details []map[string]interface{} `json:"details"`
|
||||||
|
InnerError map[string]interface{} `json:"innererror"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceError2 struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Target *string `json:"target"`
|
||||||
|
Details map[string]interface{} `json:"details"`
|
||||||
|
InnerError map[string]interface{} `json:"innererror"`
|
||||||
|
}
|
||||||
|
|
||||||
|
se1 := serviceError1{}
|
||||||
|
err := json.Unmarshal(b, &se1)
|
||||||
|
if err == nil {
|
||||||
|
se.populate(se1.Code, se1.Message, se1.Target, se1.Details, se1.InnerError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
se2 := serviceError2{}
|
||||||
|
err = json.Unmarshal(b, &se2)
|
||||||
|
if err == nil {
|
||||||
|
se.populate(se2.Code, se2.Message, se2.Target, nil, se2.InnerError)
|
||||||
|
se.Details = append(se.Details, se2.Details)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (se *ServiceError) populate(code, message string, target *string, details []map[string]interface{}, inner map[string]interface{}) {
|
||||||
|
se.Code = code
|
||||||
|
se.Message = message
|
||||||
|
se.Target = target
|
||||||
|
se.Details = details
|
||||||
|
se.InnerError = inner
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestError describes an error response returned by Azure service.
|
||||||
|
type RequestError struct {
|
||||||
|
autorest.DetailedError
|
||||||
|
|
||||||
|
// The error returned by the Azure service.
|
||||||
|
ServiceError *ServiceError `json:"error"`
|
||||||
|
|
||||||
|
// The request id (from the x-ms-request-id-header) of the request.
|
||||||
|
RequestID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a human-friendly error message from service error.
|
||||||
|
func (e RequestError) Error() string {
|
||||||
|
return fmt.Sprintf("autorest/azure: Service returned an error. Status=%v %v",
|
||||||
|
e.StatusCode, e.ServiceError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAzureError returns true if the passed error is an Azure Service error; false otherwise.
|
||||||
|
func IsAzureError(e error) bool {
|
||||||
|
_, ok := e.(*RequestError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resource contains details about an Azure resource.
|
||||||
|
type Resource struct {
|
||||||
|
SubscriptionID string
|
||||||
|
ResourceGroup string
|
||||||
|
Provider string
|
||||||
|
ResourceType string
|
||||||
|
ResourceName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseResourceID parses a resource ID into a ResourceDetails struct.
|
||||||
|
// See https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-template-functions-resource#return-value-4.
|
||||||
|
func ParseResourceID(resourceID string) (Resource, error) {
|
||||||
|
|
||||||
|
const resourceIDPatternText = `(?i)subscriptions/(.+)/resourceGroups/(.+)/providers/(.+?)/(.+?)/(.+)`
|
||||||
|
resourceIDPattern := regexp.MustCompile(resourceIDPatternText)
|
||||||
|
match := resourceIDPattern.FindStringSubmatch(resourceID)
|
||||||
|
|
||||||
|
if len(match) == 0 {
|
||||||
|
return Resource{}, fmt.Errorf("parsing failed for %s. Invalid resource Id format", resourceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := strings.Split(match[5], "/")
|
||||||
|
resourceName := v[len(v)-1]
|
||||||
|
|
||||||
|
result := Resource{
|
||||||
|
SubscriptionID: match[1],
|
||||||
|
ResourceGroup: match[2],
|
||||||
|
Provider: match[3],
|
||||||
|
ResourceType: match[4],
|
||||||
|
ResourceName: resourceName,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorWithError creates a new Error conforming object from the
|
||||||
|
// passed packageType, method, statusCode of the given resp (UndefinedStatusCode
|
||||||
|
// if resp is nil), message, and original error. message is treated as a format
|
||||||
|
// string to which the optional args apply.
|
||||||
|
func NewErrorWithError(original error, packageType string, method string, resp *http.Response, message string, args ...interface{}) RequestError {
|
||||||
|
if v, ok := original.(*RequestError); ok {
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode := autorest.UndefinedStatusCode
|
||||||
|
if resp != nil {
|
||||||
|
statusCode = resp.StatusCode
|
||||||
|
}
|
||||||
|
return RequestError{
|
||||||
|
DetailedError: autorest.DetailedError{
|
||||||
|
Original: original,
|
||||||
|
PackageType: packageType,
|
||||||
|
Method: method,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
Message: fmt.Sprintf(message, args...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithReturningClientID returns a PrepareDecorator that adds an HTTP extension header of
|
||||||
|
// x-ms-client-request-id whose value is the passed, undecorated UUID (e.g.,
|
||||||
|
// "0F39878C-5F76-4DB8-A25D-61D2C193C3CA"). It also sets the x-ms-return-client-request-id
|
||||||
|
// header to true such that UUID accompanies the http.Response.
|
||||||
|
func WithReturningClientID(uuid string) autorest.PrepareDecorator {
|
||||||
|
preparer := autorest.CreatePreparer(
|
||||||
|
WithClientID(uuid),
|
||||||
|
WithReturnClientID(true))
|
||||||
|
|
||||||
|
return func(p autorest.Preparer) autorest.Preparer {
|
||||||
|
return autorest.PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
return preparer.Prepare(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClientID returns a PrepareDecorator that adds an HTTP extension header of
|
||||||
|
// x-ms-client-request-id whose value is passed, undecorated UUID (e.g.,
|
||||||
|
// "0F39878C-5F76-4DB8-A25D-61D2C193C3CA").
|
||||||
|
func WithClientID(uuid string) autorest.PrepareDecorator {
|
||||||
|
return autorest.WithHeader(HeaderClientID, uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithReturnClientID returns a PrepareDecorator that adds an HTTP extension header of
|
||||||
|
// x-ms-return-client-request-id whose boolean value indicates if the value of the
|
||||||
|
// x-ms-client-request-id header should be included in the http.Response.
|
||||||
|
func WithReturnClientID(b bool) autorest.PrepareDecorator {
|
||||||
|
return autorest.WithHeader(HeaderReturnClientID, strconv.FormatBool(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractClientID extracts the client identifier from the x-ms-client-request-id header set on the
|
||||||
|
// http.Request sent to the service (and returned in the http.Response)
|
||||||
|
func ExtractClientID(resp *http.Response) string {
|
||||||
|
return autorest.ExtractHeaderValue(HeaderClientID, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractRequestID extracts the Azure server generated request identifier from the
|
||||||
|
// x-ms-request-id header.
|
||||||
|
func ExtractRequestID(resp *http.Response) string {
|
||||||
|
return autorest.ExtractHeaderValue(HeaderRequestID, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithErrorUnlessStatusCode returns a RespondDecorator that emits an
|
||||||
|
// azure.RequestError by reading the response body unless the response HTTP status code
|
||||||
|
// is among the set passed.
|
||||||
|
//
|
||||||
|
// If there is a chance service may return responses other than the Azure error
|
||||||
|
// format and the response cannot be parsed into an error, a decoding error will
|
||||||
|
// be returned containing the response body. In any case, the Responder will
|
||||||
|
// return an error if the status code is not satisfied.
|
||||||
|
//
|
||||||
|
// If this Responder returns an error, the response body will be replaced with
|
||||||
|
// an in-memory reader, which needs no further closing.
|
||||||
|
func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator {
|
||||||
|
return func(r autorest.Responder) autorest.Responder {
|
||||||
|
return autorest.ResponderFunc(func(resp *http.Response) error {
|
||||||
|
err := r.Respond(resp)
|
||||||
|
if err == nil && !autorest.ResponseHasStatusCode(resp, codes...) {
|
||||||
|
var e RequestError
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Copy and replace the Body in case it does not contain an error object.
|
||||||
|
// This will leave the Body available to the caller.
|
||||||
|
b, decodeErr := autorest.CopyAndDecode(autorest.EncodedAsJSON, resp.Body, &e)
|
||||||
|
resp.Body = ioutil.NopCloser(&b)
|
||||||
|
if decodeErr != nil {
|
||||||
|
return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr)
|
||||||
|
}
|
||||||
|
if e.ServiceError == nil {
|
||||||
|
// Check if error is unwrapped ServiceError
|
||||||
|
if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.ServiceError.Message == "" {
|
||||||
|
// if we're here it means the returned error wasn't OData v4 compliant.
|
||||||
|
// try to unmarshal the body as raw JSON in hopes of getting something.
|
||||||
|
rawBody := map[string]interface{}{}
|
||||||
|
if err := json.Unmarshal(b.Bytes(), &rawBody); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.ServiceError = &ServiceError{
|
||||||
|
Code: "Unknown",
|
||||||
|
Message: "Unknown service error",
|
||||||
|
}
|
||||||
|
if len(rawBody) > 0 {
|
||||||
|
e.ServiceError.Details = []map[string]interface{}{rawBody}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.Response = resp
|
||||||
|
e.RequestID = ExtractRequestID(resp)
|
||||||
|
if e.StatusCode == nil {
|
||||||
|
e.StatusCode = resp.StatusCode
|
||||||
|
}
|
||||||
|
err = &e
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
package azure
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvironmentFilepathName captures the name of the environment variable containing the path to the file
|
||||||
|
// to be used while populating the Azure Environment.
|
||||||
|
const EnvironmentFilepathName = "AZURE_ENVIRONMENT_FILEPATH"
|
||||||
|
|
||||||
|
var environments = map[string]Environment{
|
||||||
|
"AZURECHINACLOUD": ChinaCloud,
|
||||||
|
"AZUREGERMANCLOUD": GermanCloud,
|
||||||
|
"AZUREPUBLICCLOUD": PublicCloud,
|
||||||
|
"AZUREUSGOVERNMENTCLOUD": USGovernmentCloud,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment represents a set of endpoints for each of Azure's Clouds.
|
||||||
|
type Environment struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ManagementPortalURL string `json:"managementPortalURL"`
|
||||||
|
PublishSettingsURL string `json:"publishSettingsURL"`
|
||||||
|
ServiceManagementEndpoint string `json:"serviceManagementEndpoint"`
|
||||||
|
ResourceManagerEndpoint string `json:"resourceManagerEndpoint"`
|
||||||
|
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
|
||||||
|
GalleryEndpoint string `json:"galleryEndpoint"`
|
||||||
|
KeyVaultEndpoint string `json:"keyVaultEndpoint"`
|
||||||
|
GraphEndpoint string `json:"graphEndpoint"`
|
||||||
|
ServiceBusEndpoint string `json:"serviceBusEndpoint"`
|
||||||
|
BatchManagementEndpoint string `json:"batchManagementEndpoint"`
|
||||||
|
StorageEndpointSuffix string `json:"storageEndpointSuffix"`
|
||||||
|
SQLDatabaseDNSSuffix string `json:"sqlDatabaseDNSSuffix"`
|
||||||
|
TrafficManagerDNSSuffix string `json:"trafficManagerDNSSuffix"`
|
||||||
|
KeyVaultDNSSuffix string `json:"keyVaultDNSSuffix"`
|
||||||
|
ServiceBusEndpointSuffix string `json:"serviceBusEndpointSuffix"`
|
||||||
|
ServiceManagementVMDNSSuffix string `json:"serviceManagementVMDNSSuffix"`
|
||||||
|
ResourceManagerVMDNSSuffix string `json:"resourceManagerVMDNSSuffix"`
|
||||||
|
ContainerRegistryDNSSuffix string `json:"containerRegistryDNSSuffix"`
|
||||||
|
TokenAudience string `json:"tokenAudience"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// PublicCloud is the default public Azure cloud environment
|
||||||
|
PublicCloud = Environment{
|
||||||
|
Name: "AzurePublicCloud",
|
||||||
|
ManagementPortalURL: "https://manage.windowsazure.com/",
|
||||||
|
PublishSettingsURL: "https://manage.windowsazure.com/publishsettings/index",
|
||||||
|
ServiceManagementEndpoint: "https://management.core.windows.net/",
|
||||||
|
ResourceManagerEndpoint: "https://management.azure.com/",
|
||||||
|
ActiveDirectoryEndpoint: "https://login.microsoftonline.com/",
|
||||||
|
GalleryEndpoint: "https://gallery.azure.com/",
|
||||||
|
KeyVaultEndpoint: "https://vault.azure.net/",
|
||||||
|
GraphEndpoint: "https://graph.windows.net/",
|
||||||
|
ServiceBusEndpoint: "https://servicebus.windows.net/",
|
||||||
|
BatchManagementEndpoint: "https://batch.core.windows.net/",
|
||||||
|
StorageEndpointSuffix: "core.windows.net",
|
||||||
|
SQLDatabaseDNSSuffix: "database.windows.net",
|
||||||
|
TrafficManagerDNSSuffix: "trafficmanager.net",
|
||||||
|
KeyVaultDNSSuffix: "vault.azure.net",
|
||||||
|
ServiceBusEndpointSuffix: "servicebus.windows.net",
|
||||||
|
ServiceManagementVMDNSSuffix: "cloudapp.net",
|
||||||
|
ResourceManagerVMDNSSuffix: "cloudapp.azure.com",
|
||||||
|
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||||
|
TokenAudience: "https://management.azure.com/",
|
||||||
|
}
|
||||||
|
|
||||||
|
// USGovernmentCloud is the cloud environment for the US Government
|
||||||
|
USGovernmentCloud = Environment{
|
||||||
|
Name: "AzureUSGovernmentCloud",
|
||||||
|
ManagementPortalURL: "https://manage.windowsazure.us/",
|
||||||
|
PublishSettingsURL: "https://manage.windowsazure.us/publishsettings/index",
|
||||||
|
ServiceManagementEndpoint: "https://management.core.usgovcloudapi.net/",
|
||||||
|
ResourceManagerEndpoint: "https://management.usgovcloudapi.net/",
|
||||||
|
ActiveDirectoryEndpoint: "https://login.microsoftonline.us/",
|
||||||
|
GalleryEndpoint: "https://gallery.usgovcloudapi.net/",
|
||||||
|
KeyVaultEndpoint: "https://vault.usgovcloudapi.net/",
|
||||||
|
GraphEndpoint: "https://graph.windows.net/",
|
||||||
|
ServiceBusEndpoint: "https://servicebus.usgovcloudapi.net/",
|
||||||
|
BatchManagementEndpoint: "https://batch.core.usgovcloudapi.net/",
|
||||||
|
StorageEndpointSuffix: "core.usgovcloudapi.net",
|
||||||
|
SQLDatabaseDNSSuffix: "database.usgovcloudapi.net",
|
||||||
|
TrafficManagerDNSSuffix: "usgovtrafficmanager.net",
|
||||||
|
KeyVaultDNSSuffix: "vault.usgovcloudapi.net",
|
||||||
|
ServiceBusEndpointSuffix: "servicebus.usgovcloudapi.net",
|
||||||
|
ServiceManagementVMDNSSuffix: "usgovcloudapp.net",
|
||||||
|
ResourceManagerVMDNSSuffix: "cloudapp.windowsazure.us",
|
||||||
|
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||||
|
TokenAudience: "https://management.usgovcloudapi.net/",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChinaCloud is the cloud environment operated in China
|
||||||
|
ChinaCloud = Environment{
|
||||||
|
Name: "AzureChinaCloud",
|
||||||
|
ManagementPortalURL: "https://manage.chinacloudapi.com/",
|
||||||
|
PublishSettingsURL: "https://manage.chinacloudapi.com/publishsettings/index",
|
||||||
|
ServiceManagementEndpoint: "https://management.core.chinacloudapi.cn/",
|
||||||
|
ResourceManagerEndpoint: "https://management.chinacloudapi.cn/",
|
||||||
|
ActiveDirectoryEndpoint: "https://login.chinacloudapi.cn/",
|
||||||
|
GalleryEndpoint: "https://gallery.chinacloudapi.cn/",
|
||||||
|
KeyVaultEndpoint: "https://vault.azure.cn/",
|
||||||
|
GraphEndpoint: "https://graph.chinacloudapi.cn/",
|
||||||
|
ServiceBusEndpoint: "https://servicebus.chinacloudapi.cn/",
|
||||||
|
BatchManagementEndpoint: "https://batch.chinacloudapi.cn/",
|
||||||
|
StorageEndpointSuffix: "core.chinacloudapi.cn",
|
||||||
|
SQLDatabaseDNSSuffix: "database.chinacloudapi.cn",
|
||||||
|
TrafficManagerDNSSuffix: "trafficmanager.cn",
|
||||||
|
KeyVaultDNSSuffix: "vault.azure.cn",
|
||||||
|
ServiceBusEndpointSuffix: "servicebus.chinacloudapi.cn",
|
||||||
|
ServiceManagementVMDNSSuffix: "chinacloudapp.cn",
|
||||||
|
ResourceManagerVMDNSSuffix: "cloudapp.azure.cn",
|
||||||
|
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||||
|
TokenAudience: "https://management.chinacloudapi.cn/",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GermanCloud is the cloud environment operated in Germany
|
||||||
|
GermanCloud = Environment{
|
||||||
|
Name: "AzureGermanCloud",
|
||||||
|
ManagementPortalURL: "http://portal.microsoftazure.de/",
|
||||||
|
PublishSettingsURL: "https://manage.microsoftazure.de/publishsettings/index",
|
||||||
|
ServiceManagementEndpoint: "https://management.core.cloudapi.de/",
|
||||||
|
ResourceManagerEndpoint: "https://management.microsoftazure.de/",
|
||||||
|
ActiveDirectoryEndpoint: "https://login.microsoftonline.de/",
|
||||||
|
GalleryEndpoint: "https://gallery.cloudapi.de/",
|
||||||
|
KeyVaultEndpoint: "https://vault.microsoftazure.de/",
|
||||||
|
GraphEndpoint: "https://graph.cloudapi.de/",
|
||||||
|
ServiceBusEndpoint: "https://servicebus.cloudapi.de/",
|
||||||
|
BatchManagementEndpoint: "https://batch.cloudapi.de/",
|
||||||
|
StorageEndpointSuffix: "core.cloudapi.de",
|
||||||
|
SQLDatabaseDNSSuffix: "database.cloudapi.de",
|
||||||
|
TrafficManagerDNSSuffix: "azuretrafficmanager.de",
|
||||||
|
KeyVaultDNSSuffix: "vault.microsoftazure.de",
|
||||||
|
ServiceBusEndpointSuffix: "servicebus.cloudapi.de",
|
||||||
|
ServiceManagementVMDNSSuffix: "azurecloudapp.de",
|
||||||
|
ResourceManagerVMDNSSuffix: "cloudapp.microsoftazure.de",
|
||||||
|
ContainerRegistryDNSSuffix: "azurecr.io",
|
||||||
|
TokenAudience: "https://management.microsoftazure.de/",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvironmentFromName returns an Environment based on the common name specified.
|
||||||
|
func EnvironmentFromName(name string) (Environment, error) {
|
||||||
|
// IMPORTANT
|
||||||
|
// As per @radhikagupta5:
|
||||||
|
// This is technical debt, fundamentally here because Kubernetes is not currently accepting
|
||||||
|
// contributions to the providers. Once that is an option, the provider should be updated to
|
||||||
|
// directly call `EnvironmentFromFile`. Until then, we rely on dispatching Azure Stack environment creation
|
||||||
|
// from this method based on the name that is provided to us.
|
||||||
|
if strings.EqualFold(name, "AZURESTACKCLOUD") {
|
||||||
|
return EnvironmentFromFile(os.Getenv(EnvironmentFilepathName))
|
||||||
|
}
|
||||||
|
|
||||||
|
name = strings.ToUpper(name)
|
||||||
|
env, ok := environments[name]
|
||||||
|
if !ok {
|
||||||
|
return env, fmt.Errorf("autorest/azure: There is no cloud environment matching the name %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return env, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvironmentFromFile loads an Environment from a configuration file available on disk.
|
||||||
|
// This function is particularly useful in the Hybrid Cloud model, where one must define their own
|
||||||
|
// endpoints.
|
||||||
|
func EnvironmentFromFile(location string) (unmarshaled Environment, err error) {
|
||||||
|
fileContents, err := ioutil.ReadFile(location)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(fileContents, &unmarshaled)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
package azure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
type audience []string
|
||||||
|
|
||||||
|
type authentication struct {
|
||||||
|
LoginEndpoint string `json:"loginEndpoint"`
|
||||||
|
Audiences audience `json:"audiences"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type environmentMetadataInfo struct {
|
||||||
|
GalleryEndpoint string `json:"galleryEndpoint"`
|
||||||
|
GraphEndpoint string `json:"graphEndpoint"`
|
||||||
|
PortalEndpoint string `json:"portalEndpoint"`
|
||||||
|
Authentication authentication `json:"authentication"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvironmentProperty represent property names that clients can override
|
||||||
|
type EnvironmentProperty string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EnvironmentName ...
|
||||||
|
EnvironmentName EnvironmentProperty = "name"
|
||||||
|
// EnvironmentManagementPortalURL ..
|
||||||
|
EnvironmentManagementPortalURL EnvironmentProperty = "managementPortalURL"
|
||||||
|
// EnvironmentPublishSettingsURL ...
|
||||||
|
EnvironmentPublishSettingsURL EnvironmentProperty = "publishSettingsURL"
|
||||||
|
// EnvironmentServiceManagementEndpoint ...
|
||||||
|
EnvironmentServiceManagementEndpoint EnvironmentProperty = "serviceManagementEndpoint"
|
||||||
|
// EnvironmentResourceManagerEndpoint ...
|
||||||
|
EnvironmentResourceManagerEndpoint EnvironmentProperty = "resourceManagerEndpoint"
|
||||||
|
// EnvironmentActiveDirectoryEndpoint ...
|
||||||
|
EnvironmentActiveDirectoryEndpoint EnvironmentProperty = "activeDirectoryEndpoint"
|
||||||
|
// EnvironmentGalleryEndpoint ...
|
||||||
|
EnvironmentGalleryEndpoint EnvironmentProperty = "galleryEndpoint"
|
||||||
|
// EnvironmentKeyVaultEndpoint ...
|
||||||
|
EnvironmentKeyVaultEndpoint EnvironmentProperty = "keyVaultEndpoint"
|
||||||
|
// EnvironmentGraphEndpoint ...
|
||||||
|
EnvironmentGraphEndpoint EnvironmentProperty = "graphEndpoint"
|
||||||
|
// EnvironmentServiceBusEndpoint ...
|
||||||
|
EnvironmentServiceBusEndpoint EnvironmentProperty = "serviceBusEndpoint"
|
||||||
|
// EnvironmentBatchManagementEndpoint ...
|
||||||
|
EnvironmentBatchManagementEndpoint EnvironmentProperty = "batchManagementEndpoint"
|
||||||
|
// EnvironmentStorageEndpointSuffix ...
|
||||||
|
EnvironmentStorageEndpointSuffix EnvironmentProperty = "storageEndpointSuffix"
|
||||||
|
// EnvironmentSQLDatabaseDNSSuffix ...
|
||||||
|
EnvironmentSQLDatabaseDNSSuffix EnvironmentProperty = "sqlDatabaseDNSSuffix"
|
||||||
|
// EnvironmentTrafficManagerDNSSuffix ...
|
||||||
|
EnvironmentTrafficManagerDNSSuffix EnvironmentProperty = "trafficManagerDNSSuffix"
|
||||||
|
// EnvironmentKeyVaultDNSSuffix ...
|
||||||
|
EnvironmentKeyVaultDNSSuffix EnvironmentProperty = "keyVaultDNSSuffix"
|
||||||
|
// EnvironmentServiceBusEndpointSuffix ...
|
||||||
|
EnvironmentServiceBusEndpointSuffix EnvironmentProperty = "serviceBusEndpointSuffix"
|
||||||
|
// EnvironmentServiceManagementVMDNSSuffix ...
|
||||||
|
EnvironmentServiceManagementVMDNSSuffix EnvironmentProperty = "serviceManagementVMDNSSuffix"
|
||||||
|
// EnvironmentResourceManagerVMDNSSuffix ...
|
||||||
|
EnvironmentResourceManagerVMDNSSuffix EnvironmentProperty = "resourceManagerVMDNSSuffix"
|
||||||
|
// EnvironmentContainerRegistryDNSSuffix ...
|
||||||
|
EnvironmentContainerRegistryDNSSuffix EnvironmentProperty = "containerRegistryDNSSuffix"
|
||||||
|
// EnvironmentTokenAudience ...
|
||||||
|
EnvironmentTokenAudience EnvironmentProperty = "tokenAudience"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OverrideProperty represents property name and value that clients can override
|
||||||
|
type OverrideProperty struct {
|
||||||
|
Key EnvironmentProperty
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvironmentFromURL loads an Environment from a URL
|
||||||
|
// This function is particularly useful in the Hybrid Cloud model, where one may define their own
|
||||||
|
// endpoints.
|
||||||
|
func EnvironmentFromURL(resourceManagerEndpoint string, properties ...OverrideProperty) (environment Environment, err error) {
|
||||||
|
var metadataEnvProperties environmentMetadataInfo
|
||||||
|
|
||||||
|
if resourceManagerEndpoint == "" {
|
||||||
|
return environment, fmt.Errorf("Metadata resource manager endpoint is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadataEnvProperties, err = retrieveMetadataEnvironment(resourceManagerEndpoint); err != nil {
|
||||||
|
return environment, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give priority to user's override values
|
||||||
|
overrideProperties(&environment, properties)
|
||||||
|
|
||||||
|
if environment.Name == "" {
|
||||||
|
environment.Name = "HybridEnvironment"
|
||||||
|
}
|
||||||
|
stampDNSSuffix := environment.StorageEndpointSuffix
|
||||||
|
if stampDNSSuffix == "" {
|
||||||
|
stampDNSSuffix = strings.TrimSuffix(strings.TrimPrefix(strings.Replace(resourceManagerEndpoint, strings.Split(resourceManagerEndpoint, ".")[0], "", 1), "."), "/")
|
||||||
|
environment.StorageEndpointSuffix = stampDNSSuffix
|
||||||
|
}
|
||||||
|
if environment.KeyVaultDNSSuffix == "" {
|
||||||
|
environment.KeyVaultDNSSuffix = fmt.Sprintf("%s.%s", "vault", stampDNSSuffix)
|
||||||
|
}
|
||||||
|
if environment.KeyVaultEndpoint == "" {
|
||||||
|
environment.KeyVaultEndpoint = fmt.Sprintf("%s%s", "https://", environment.KeyVaultDNSSuffix)
|
||||||
|
}
|
||||||
|
if environment.TokenAudience == "" {
|
||||||
|
environment.TokenAudience = metadataEnvProperties.Authentication.Audiences[0]
|
||||||
|
}
|
||||||
|
if environment.ActiveDirectoryEndpoint == "" {
|
||||||
|
environment.ActiveDirectoryEndpoint = metadataEnvProperties.Authentication.LoginEndpoint
|
||||||
|
}
|
||||||
|
if environment.ResourceManagerEndpoint == "" {
|
||||||
|
environment.ResourceManagerEndpoint = resourceManagerEndpoint
|
||||||
|
}
|
||||||
|
if environment.GalleryEndpoint == "" {
|
||||||
|
environment.GalleryEndpoint = metadataEnvProperties.GalleryEndpoint
|
||||||
|
}
|
||||||
|
if environment.GraphEndpoint == "" {
|
||||||
|
environment.GraphEndpoint = metadataEnvProperties.GraphEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
return environment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func overrideProperties(environment *Environment, properties []OverrideProperty) {
|
||||||
|
for _, property := range properties {
|
||||||
|
switch property.Key {
|
||||||
|
case EnvironmentName:
|
||||||
|
{
|
||||||
|
environment.Name = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentManagementPortalURL:
|
||||||
|
{
|
||||||
|
environment.ManagementPortalURL = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentPublishSettingsURL:
|
||||||
|
{
|
||||||
|
environment.PublishSettingsURL = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentServiceManagementEndpoint:
|
||||||
|
{
|
||||||
|
environment.ServiceManagementEndpoint = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentResourceManagerEndpoint:
|
||||||
|
{
|
||||||
|
environment.ResourceManagerEndpoint = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentActiveDirectoryEndpoint:
|
||||||
|
{
|
||||||
|
environment.ActiveDirectoryEndpoint = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentGalleryEndpoint:
|
||||||
|
{
|
||||||
|
environment.GalleryEndpoint = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentKeyVaultEndpoint:
|
||||||
|
{
|
||||||
|
environment.KeyVaultEndpoint = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentGraphEndpoint:
|
||||||
|
{
|
||||||
|
environment.GraphEndpoint = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentServiceBusEndpoint:
|
||||||
|
{
|
||||||
|
environment.ServiceBusEndpoint = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentBatchManagementEndpoint:
|
||||||
|
{
|
||||||
|
environment.BatchManagementEndpoint = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentStorageEndpointSuffix:
|
||||||
|
{
|
||||||
|
environment.StorageEndpointSuffix = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentSQLDatabaseDNSSuffix:
|
||||||
|
{
|
||||||
|
environment.SQLDatabaseDNSSuffix = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentTrafficManagerDNSSuffix:
|
||||||
|
{
|
||||||
|
environment.TrafficManagerDNSSuffix = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentKeyVaultDNSSuffix:
|
||||||
|
{
|
||||||
|
environment.KeyVaultDNSSuffix = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentServiceBusEndpointSuffix:
|
||||||
|
{
|
||||||
|
environment.ServiceBusEndpointSuffix = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentServiceManagementVMDNSSuffix:
|
||||||
|
{
|
||||||
|
environment.ServiceManagementVMDNSSuffix = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentResourceManagerVMDNSSuffix:
|
||||||
|
{
|
||||||
|
environment.ResourceManagerVMDNSSuffix = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentContainerRegistryDNSSuffix:
|
||||||
|
{
|
||||||
|
environment.ContainerRegistryDNSSuffix = property.Value
|
||||||
|
}
|
||||||
|
case EnvironmentTokenAudience:
|
||||||
|
{
|
||||||
|
environment.TokenAudience = property.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveMetadataEnvironment(endpoint string) (environment environmentMetadataInfo, err error) {
|
||||||
|
client := autorest.NewClientWithUserAgent("")
|
||||||
|
managementEndpoint := fmt.Sprintf("%s%s", strings.TrimSuffix(endpoint, "/"), "/metadata/endpoints?api-version=1.0")
|
||||||
|
req, _ := http.NewRequest("GET", managementEndpoint, nil)
|
||||||
|
response, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return environment, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
jsonResponse, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return environment, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(jsonResponse, &environment)
|
||||||
|
return environment, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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 azure
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Azure/go-autorest/autorest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DoRetryWithRegistration tries to register the resource provider in case it is unregistered.
|
||||||
|
// It also handles request retries
|
||||||
|
func DoRetryWithRegistration(client autorest.Client) autorest.SendDecorator {
|
||||||
|
return func(s autorest.Sender) autorest.Sender {
|
||||||
|
return autorest.SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
||||||
|
rr := autorest.NewRetriableRequest(r)
|
||||||
|
for currentAttempt := 0; currentAttempt < client.RetryAttempts; currentAttempt++ {
|
||||||
|
err = rr.Prepare()
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = autorest.SendWithSender(s, rr.Request(),
|
||||||
|
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusConflict || client.SkipResourceProviderRegistration {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
var re RequestError
|
||||||
|
err = autorest.Respond(
|
||||||
|
resp,
|
||||||
|
autorest.ByUnmarshallingJSON(&re),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
err = re
|
||||||
|
|
||||||
|
if re.ServiceError != nil && re.ServiceError.Code == "MissingSubscriptionRegistration" {
|
||||||
|
regErr := register(client, r, re)
|
||||||
|
if regErr != nil {
|
||||||
|
return resp, fmt.Errorf("failed auto registering Resource Provider: %s. Original error: %s", regErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProvider(re RequestError) (string, error) {
|
||||||
|
if re.ServiceError != nil && len(re.ServiceError.Details) > 0 {
|
||||||
|
return re.ServiceError.Details[0]["target"].(string), nil
|
||||||
|
}
|
||||||
|
return "", errors.New("provider was not found in the response")
|
||||||
|
}
|
||||||
|
|
||||||
|
func register(client autorest.Client, originalReq *http.Request, re RequestError) error {
|
||||||
|
subID := getSubscription(originalReq.URL.Path)
|
||||||
|
if subID == "" {
|
||||||
|
return errors.New("missing parameter subscriptionID to register resource provider")
|
||||||
|
}
|
||||||
|
providerName, err := getProvider(re)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("missing parameter provider to register resource provider: %s", err)
|
||||||
|
}
|
||||||
|
newURL := url.URL{
|
||||||
|
Scheme: originalReq.URL.Scheme,
|
||||||
|
Host: originalReq.URL.Host,
|
||||||
|
}
|
||||||
|
|
||||||
|
// taken from the resources SDK
|
||||||
|
// with almost identical code, this sections are easier to mantain
|
||||||
|
// It is also not a good idea to import the SDK here
|
||||||
|
// https://github.com/Azure/azure-sdk-for-go/blob/9f366792afa3e0ddaecdc860e793ba9d75e76c27/arm/resources/resources/providers.go#L252
|
||||||
|
pathParameters := map[string]interface{}{
|
||||||
|
"resourceProviderNamespace": autorest.Encode("path", providerName),
|
||||||
|
"subscriptionId": autorest.Encode("path", subID),
|
||||||
|
}
|
||||||
|
|
||||||
|
const APIVersion = "2016-09-01"
|
||||||
|
queryParameters := map[string]interface{}{
|
||||||
|
"api-version": APIVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
preparer := autorest.CreatePreparer(
|
||||||
|
autorest.AsPost(),
|
||||||
|
autorest.WithBaseURL(newURL.String()),
|
||||||
|
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}/register", pathParameters),
|
||||||
|
autorest.WithQueryParameters(queryParameters),
|
||||||
|
)
|
||||||
|
|
||||||
|
req, err := preparer.Prepare(&http.Request{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req = req.WithContext(originalReq.Context())
|
||||||
|
|
||||||
|
resp, err := autorest.SendWithSender(client, req,
|
||||||
|
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Provider struct {
|
||||||
|
RegistrationState *string `json:"registrationState,omitempty"`
|
||||||
|
}
|
||||||
|
var provider Provider
|
||||||
|
|
||||||
|
err = autorest.Respond(
|
||||||
|
resp,
|
||||||
|
WithErrorUnlessStatusCode(http.StatusOK),
|
||||||
|
autorest.ByUnmarshallingJSON(&provider),
|
||||||
|
autorest.ByClosing(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// poll for registered provisioning state
|
||||||
|
now := time.Now()
|
||||||
|
for err == nil && time.Since(now) < client.PollingDuration {
|
||||||
|
// taken from the resources SDK
|
||||||
|
// https://github.com/Azure/azure-sdk-for-go/blob/9f366792afa3e0ddaecdc860e793ba9d75e76c27/arm/resources/resources/providers.go#L45
|
||||||
|
preparer := autorest.CreatePreparer(
|
||||||
|
autorest.AsGet(),
|
||||||
|
autorest.WithBaseURL(newURL.String()),
|
||||||
|
autorest.WithPathParameters("/subscriptions/{subscriptionId}/providers/{resourceProviderNamespace}", pathParameters),
|
||||||
|
autorest.WithQueryParameters(queryParameters),
|
||||||
|
)
|
||||||
|
req, err = preparer.Prepare(&http.Request{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req = req.WithContext(originalReq.Context())
|
||||||
|
|
||||||
|
resp, err := autorest.SendWithSender(client, req,
|
||||||
|
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = autorest.Respond(
|
||||||
|
resp,
|
||||||
|
WithErrorUnlessStatusCode(http.StatusOK),
|
||||||
|
autorest.ByUnmarshallingJSON(&provider),
|
||||||
|
autorest.ByClosing(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider.RegistrationState != nil &&
|
||||||
|
*provider.RegistrationState == "Registered" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
delayed := autorest.DelayWithRetryAfter(resp, originalReq.Context().Done())
|
||||||
|
if !delayed && !autorest.DelayForBackoff(client.PollingDelay, 0, originalReq.Context().Done()) {
|
||||||
|
return originalReq.Context().Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !(time.Since(now) < client.PollingDuration) {
|
||||||
|
return errors.New("polling for resource provider registration has exceeded the polling duration")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSubscription(path string) string {
|
||||||
|
parts := strings.Split(path, "/")
|
||||||
|
for i, v := range parts {
|
||||||
|
if v == "subscriptions" && (i+1) < len(parts) {
|
||||||
|
return parts[i+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,264 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultPollingDelay is a reasonable delay between polling requests.
|
||||||
|
DefaultPollingDelay = 60 * time.Second
|
||||||
|
|
||||||
|
// DefaultPollingDuration is a reasonable total polling duration.
|
||||||
|
DefaultPollingDuration = 15 * time.Minute
|
||||||
|
|
||||||
|
// DefaultRetryAttempts is number of attempts for retry status codes (5xx).
|
||||||
|
DefaultRetryAttempts = 3
|
||||||
|
|
||||||
|
// DefaultRetryDuration is the duration to wait between retries.
|
||||||
|
DefaultRetryDuration = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// defaultUserAgent builds a string containing the Go version, system archityecture and OS,
|
||||||
|
// and the go-autorest version.
|
||||||
|
defaultUserAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s",
|
||||||
|
runtime.Version(),
|
||||||
|
runtime.GOARCH,
|
||||||
|
runtime.GOOS,
|
||||||
|
Version(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusCodesForRetry are a defined group of status code for which the client will retry
|
||||||
|
StatusCodesForRetry = []int{
|
||||||
|
http.StatusRequestTimeout, // 408
|
||||||
|
http.StatusTooManyRequests, // 429
|
||||||
|
http.StatusInternalServerError, // 500
|
||||||
|
http.StatusBadGateway, // 502
|
||||||
|
http.StatusServiceUnavailable, // 503
|
||||||
|
http.StatusGatewayTimeout, // 504
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
requestFormat = `HTTP Request Begin ===================================================
|
||||||
|
%s
|
||||||
|
===================================================== HTTP Request End
|
||||||
|
`
|
||||||
|
responseFormat = `HTTP Response Begin ===================================================
|
||||||
|
%s
|
||||||
|
===================================================== HTTP Response End
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Response serves as the base for all responses from generated clients. It provides access to the
|
||||||
|
// last http.Response.
|
||||||
|
type Response struct {
|
||||||
|
*http.Response `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggingInspector implements request and response inspectors that log the full request and
|
||||||
|
// response to a supplied log.
|
||||||
|
type LoggingInspector struct {
|
||||||
|
Logger *log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInspection returns a PrepareDecorator that emits the http.Request to the supplied logger. The
|
||||||
|
// body is restored after being emitted.
|
||||||
|
//
|
||||||
|
// Note: Since it reads the entire Body, this decorator should not be used where body streaming is
|
||||||
|
// important. It is best used to trace JSON or similar body values.
|
||||||
|
func (li LoggingInspector) WithInspection() PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
var body, b bytes.Buffer
|
||||||
|
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
r.Body = ioutil.NopCloser(io.TeeReader(r.Body, &body))
|
||||||
|
if err := r.Write(&b); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to write response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
li.Logger.Printf(requestFormat, b.String())
|
||||||
|
|
||||||
|
r.Body = ioutil.NopCloser(&body)
|
||||||
|
return p.Prepare(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByInspecting returns a RespondDecorator that emits the http.Response to the supplied logger. The
|
||||||
|
// body is restored after being emitted.
|
||||||
|
//
|
||||||
|
// Note: Since it reads the entire Body, this decorator should not be used where body streaming is
|
||||||
|
// important. It is best used to trace JSON or similar body values.
|
||||||
|
func (li LoggingInspector) ByInspecting() RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
var body, b bytes.Buffer
|
||||||
|
defer resp.Body.Close()
|
||||||
|
resp.Body = ioutil.NopCloser(io.TeeReader(resp.Body, &body))
|
||||||
|
if err := resp.Write(&b); err != nil {
|
||||||
|
return fmt.Errorf("Failed to write response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
li.Logger.Printf(responseFormat, b.String())
|
||||||
|
|
||||||
|
resp.Body = ioutil.NopCloser(&body)
|
||||||
|
return r.Respond(resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client is the base for autorest generated clients. It provides default, "do nothing"
|
||||||
|
// implementations of an Authorizer, RequestInspector, and ResponseInspector. It also returns the
|
||||||
|
// standard, undecorated http.Client as a default Sender.
|
||||||
|
//
|
||||||
|
// Generated clients should also use Error (see NewError and NewErrorWithError) for errors and
|
||||||
|
// return responses that compose with Response.
|
||||||
|
//
|
||||||
|
// Most customization of generated clients is best achieved by supplying a custom Authorizer, custom
|
||||||
|
// RequestInspector, and / or custom ResponseInspector. Users may log requests, implement circuit
|
||||||
|
// breakers (see https://msdn.microsoft.com/en-us/library/dn589784.aspx) or otherwise influence
|
||||||
|
// sending the request by providing a decorated Sender.
|
||||||
|
type Client struct {
|
||||||
|
Authorizer Authorizer
|
||||||
|
Sender Sender
|
||||||
|
RequestInspector PrepareDecorator
|
||||||
|
ResponseInspector RespondDecorator
|
||||||
|
|
||||||
|
// PollingDelay sets the polling frequency used in absence of a Retry-After HTTP header
|
||||||
|
PollingDelay time.Duration
|
||||||
|
|
||||||
|
// PollingDuration sets the maximum polling time after which an error is returned.
|
||||||
|
PollingDuration time.Duration
|
||||||
|
|
||||||
|
// RetryAttempts sets the default number of retry attempts for client.
|
||||||
|
RetryAttempts int
|
||||||
|
|
||||||
|
// RetryDuration sets the delay duration for retries.
|
||||||
|
RetryDuration time.Duration
|
||||||
|
|
||||||
|
// UserAgent, if not empty, will be set as the HTTP User-Agent header on all requests sent
|
||||||
|
// through the Do method.
|
||||||
|
UserAgent string
|
||||||
|
|
||||||
|
Jar http.CookieJar
|
||||||
|
|
||||||
|
// Set to true to skip attempted registration of resource providers (false by default).
|
||||||
|
SkipResourceProviderRegistration bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientWithUserAgent returns an instance of a Client with the UserAgent set to the passed
|
||||||
|
// string.
|
||||||
|
func NewClientWithUserAgent(ua string) Client {
|
||||||
|
c := Client{
|
||||||
|
PollingDelay: DefaultPollingDelay,
|
||||||
|
PollingDuration: DefaultPollingDuration,
|
||||||
|
RetryAttempts: DefaultRetryAttempts,
|
||||||
|
RetryDuration: DefaultRetryDuration,
|
||||||
|
UserAgent: defaultUserAgent,
|
||||||
|
}
|
||||||
|
c.Sender = c.sender()
|
||||||
|
c.AddToUserAgent(ua)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddToUserAgent adds an extension to the current user agent
|
||||||
|
func (c *Client) AddToUserAgent(extension string) error {
|
||||||
|
if extension != "" {
|
||||||
|
c.UserAgent = fmt.Sprintf("%s %s", c.UserAgent, extension)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Extension was empty, User Agent stayed as %s", c.UserAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do implements the Sender interface by invoking the active Sender after applying authorization.
|
||||||
|
// If Sender is not set, it uses a new instance of http.Client. In both cases it will, if UserAgent
|
||||||
|
// is set, apply set the User-Agent header.
|
||||||
|
func (c Client) Do(r *http.Request) (*http.Response, error) {
|
||||||
|
if r.UserAgent() == "" {
|
||||||
|
r, _ = Prepare(r,
|
||||||
|
WithUserAgent(c.UserAgent))
|
||||||
|
}
|
||||||
|
// NOTE: c.WithInspection() must be last in the list so that it can inspect all preceding operations
|
||||||
|
r, err := Prepare(r,
|
||||||
|
c.WithAuthorization(),
|
||||||
|
c.WithInspection())
|
||||||
|
if err != nil {
|
||||||
|
var resp *http.Response
|
||||||
|
if detErr, ok := err.(DetailedError); ok {
|
||||||
|
// if the authorization failed (e.g. invalid credentials) there will
|
||||||
|
// be a response associated with the error, be sure to return it.
|
||||||
|
resp = detErr.Response
|
||||||
|
}
|
||||||
|
return resp, NewErrorWithError(err, "autorest/Client", "Do", nil, "Preparing request failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := SendWithSender(c.sender(), r)
|
||||||
|
Respond(resp, c.ByInspecting())
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// sender returns the Sender to which to send requests.
|
||||||
|
func (c Client) sender() Sender {
|
||||||
|
if c.Sender == nil {
|
||||||
|
j, _ := cookiejar.New(nil)
|
||||||
|
return &http.Client{Jar: j}
|
||||||
|
}
|
||||||
|
return c.Sender
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAuthorization is a convenience method that returns the WithAuthorization PrepareDecorator
|
||||||
|
// from the current Authorizer. If not Authorizer is set, it uses the NullAuthorizer.
|
||||||
|
func (c Client) WithAuthorization() PrepareDecorator {
|
||||||
|
return c.authorizer().WithAuthorization()
|
||||||
|
}
|
||||||
|
|
||||||
|
// authorizer returns the Authorizer to use.
|
||||||
|
func (c Client) authorizer() Authorizer {
|
||||||
|
if c.Authorizer == nil {
|
||||||
|
return NullAuthorizer{}
|
||||||
|
}
|
||||||
|
return c.Authorizer
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInspection is a convenience method that passes the request to the supplied RequestInspector,
|
||||||
|
// if present, or returns the WithNothing PrepareDecorator otherwise.
|
||||||
|
func (c Client) WithInspection() PrepareDecorator {
|
||||||
|
if c.RequestInspector == nil {
|
||||||
|
return WithNothing()
|
||||||
|
}
|
||||||
|
return c.RequestInspector
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByInspecting is a convenience method that passes the response to the supplied ResponseInspector,
|
||||||
|
// if present, or returns the ByIgnoring RespondDecorator otherwise.
|
||||||
|
func (c Client) ByInspecting() RespondDecorator {
|
||||||
|
if c.ResponseInspector == nil {
|
||||||
|
return ByIgnoring()
|
||||||
|
}
|
||||||
|
return c.ResponseInspector
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
Package date provides time.Time derivatives that conform to the Swagger.io (https://swagger.io/)
|
||||||
|
defined date formats: Date and DateTime. Both types may, in most cases, be used in lieu of
|
||||||
|
time.Time types. And both convert to time.Time through a ToTime method.
|
||||||
|
*/
|
||||||
|
package date
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fullDate = "2006-01-02"
|
||||||
|
fullDateJSON = `"2006-01-02"`
|
||||||
|
dateFormat = "%04d-%02d-%02d"
|
||||||
|
jsonFormat = `"%04d-%02d-%02d"`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Date defines a type similar to time.Time but assumes a layout of RFC3339 full-date (i.e.,
|
||||||
|
// 2006-01-02).
|
||||||
|
type Date struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDate create a new Date from the passed string.
|
||||||
|
func ParseDate(date string) (d Date, err error) {
|
||||||
|
return parseDate(date, fullDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDate(date string, format string) (Date, error) {
|
||||||
|
d, err := time.Parse(format, date)
|
||||||
|
return Date{Time: d}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary preserves the Date as a byte array conforming to RFC3339 full-date (i.e.,
|
||||||
|
// 2006-01-02).
|
||||||
|
func (d Date) MarshalBinary() ([]byte, error) {
|
||||||
|
return d.MarshalText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary reconstitutes a Date saved as a byte array conforming to RFC3339 full-date (i.e.,
|
||||||
|
// 2006-01-02).
|
||||||
|
func (d *Date) UnmarshalBinary(data []byte) error {
|
||||||
|
return d.UnmarshalText(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON preserves the Date as a JSON string conforming to RFC3339 full-date (i.e.,
|
||||||
|
// 2006-01-02).
|
||||||
|
func (d Date) MarshalJSON() (json []byte, err error) {
|
||||||
|
return []byte(fmt.Sprintf(jsonFormat, d.Year(), d.Month(), d.Day())), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON reconstitutes the Date from a JSON string conforming to RFC3339 full-date (i.e.,
|
||||||
|
// 2006-01-02).
|
||||||
|
func (d *Date) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
d.Time, err = time.Parse(fullDateJSON, string(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText preserves the Date as a byte array conforming to RFC3339 full-date (i.e.,
|
||||||
|
// 2006-01-02).
|
||||||
|
func (d Date) MarshalText() (text []byte, err error) {
|
||||||
|
return []byte(fmt.Sprintf(dateFormat, d.Year(), d.Month(), d.Day())), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText reconstitutes a Date saved as a byte array conforming to RFC3339 full-date (i.e.,
|
||||||
|
// 2006-01-02).
|
||||||
|
func (d *Date) UnmarshalText(data []byte) (err error) {
|
||||||
|
d.Time, err = time.Parse(fullDate, string(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the Date formatted as an RFC3339 full-date string (i.e., 2006-01-02).
|
||||||
|
func (d Date) String() string {
|
||||||
|
return fmt.Sprintf(dateFormat, d.Year(), d.Month(), d.Day())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTime returns a Date as a time.Time
|
||||||
|
func (d Date) ToTime() time.Time {
|
||||||
|
return d.Time
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
package date
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Azure reports time in UTC but it doesn't include the 'Z' time zone suffix in some cases.
|
||||||
|
const (
|
||||||
|
azureUtcFormatJSON = `"2006-01-02T15:04:05.999999999"`
|
||||||
|
azureUtcFormat = "2006-01-02T15:04:05.999999999"
|
||||||
|
rfc3339JSON = `"` + time.RFC3339Nano + `"`
|
||||||
|
rfc3339 = time.RFC3339Nano
|
||||||
|
tzOffsetRegex = `(Z|z|\+|-)(\d+:\d+)*"*$`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Time defines a type similar to time.Time but assumes a layout of RFC3339 date-time (i.e.,
|
||||||
|
// 2006-01-02T15:04:05Z).
|
||||||
|
type Time struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary preserves the Time as a byte array conforming to RFC3339 date-time (i.e.,
|
||||||
|
// 2006-01-02T15:04:05Z).
|
||||||
|
func (t Time) MarshalBinary() ([]byte, error) {
|
||||||
|
return t.Time.MarshalText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary reconstitutes a Time saved as a byte array conforming to RFC3339 date-time
|
||||||
|
// (i.e., 2006-01-02T15:04:05Z).
|
||||||
|
func (t *Time) UnmarshalBinary(data []byte) error {
|
||||||
|
return t.UnmarshalText(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON preserves the Time as a JSON string conforming to RFC3339 date-time (i.e.,
|
||||||
|
// 2006-01-02T15:04:05Z).
|
||||||
|
func (t Time) MarshalJSON() (json []byte, err error) {
|
||||||
|
return t.Time.MarshalJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON reconstitutes the Time from a JSON string conforming to RFC3339 date-time
|
||||||
|
// (i.e., 2006-01-02T15:04:05Z).
|
||||||
|
func (t *Time) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
timeFormat := azureUtcFormatJSON
|
||||||
|
match, err := regexp.Match(tzOffsetRegex, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if match {
|
||||||
|
timeFormat = rfc3339JSON
|
||||||
|
}
|
||||||
|
t.Time, err = ParseTime(timeFormat, string(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText preserves the Time as a byte array conforming to RFC3339 date-time (i.e.,
|
||||||
|
// 2006-01-02T15:04:05Z).
|
||||||
|
func (t Time) MarshalText() (text []byte, err error) {
|
||||||
|
return t.Time.MarshalText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText reconstitutes a Time saved as a byte array conforming to RFC3339 date-time
|
||||||
|
// (i.e., 2006-01-02T15:04:05Z).
|
||||||
|
func (t *Time) UnmarshalText(data []byte) (err error) {
|
||||||
|
timeFormat := azureUtcFormat
|
||||||
|
match, err := regexp.Match(tzOffsetRegex, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if match {
|
||||||
|
timeFormat = rfc3339
|
||||||
|
}
|
||||||
|
t.Time, err = ParseTime(timeFormat, string(data))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the Time formatted as an RFC3339 date-time string (i.e.,
|
||||||
|
// 2006-01-02T15:04:05Z).
|
||||||
|
func (t Time) String() string {
|
||||||
|
// Note: time.Time.String does not return an RFC3339 compliant string, time.Time.MarshalText does.
|
||||||
|
b, err := t.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTime returns a Time as a time.Time
|
||||||
|
func (t Time) ToTime() time.Time {
|
||||||
|
return t.Time
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package date
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rfc1123JSON = `"` + time.RFC1123 + `"`
|
||||||
|
rfc1123 = time.RFC1123
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeRFC1123 defines a type similar to time.Time but assumes a layout of RFC1123 date-time (i.e.,
|
||||||
|
// Mon, 02 Jan 2006 15:04:05 MST).
|
||||||
|
type TimeRFC1123 struct {
|
||||||
|
time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON reconstitutes the Time from a JSON string conforming to RFC1123 date-time
|
||||||
|
// (i.e., Mon, 02 Jan 2006 15:04:05 MST).
|
||||||
|
func (t *TimeRFC1123) UnmarshalJSON(data []byte) (err error) {
|
||||||
|
t.Time, err = ParseTime(rfc1123JSON, string(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON preserves the Time as a JSON string conforming to RFC1123 date-time (i.e.,
|
||||||
|
// Mon, 02 Jan 2006 15:04:05 MST).
|
||||||
|
func (t TimeRFC1123) MarshalJSON() ([]byte, error) {
|
||||||
|
if y := t.Year(); y < 0 || y >= 10000 {
|
||||||
|
return nil, errors.New("Time.MarshalJSON: year outside of range [0,9999]")
|
||||||
|
}
|
||||||
|
b := []byte(t.Format(rfc1123JSON))
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText preserves the Time as a byte array conforming to RFC1123 date-time (i.e.,
|
||||||
|
// Mon, 02 Jan 2006 15:04:05 MST).
|
||||||
|
func (t TimeRFC1123) MarshalText() ([]byte, error) {
|
||||||
|
if y := t.Year(); y < 0 || y >= 10000 {
|
||||||
|
return nil, errors.New("Time.MarshalText: year outside of range [0,9999]")
|
||||||
|
}
|
||||||
|
|
||||||
|
b := []byte(t.Format(rfc1123))
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText reconstitutes a Time saved as a byte array conforming to RFC1123 date-time
|
||||||
|
// (i.e., Mon, 02 Jan 2006 15:04:05 MST).
|
||||||
|
func (t *TimeRFC1123) UnmarshalText(data []byte) (err error) {
|
||||||
|
t.Time, err = ParseTime(rfc1123, string(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary preserves the Time as a byte array conforming to RFC1123 date-time (i.e.,
|
||||||
|
// Mon, 02 Jan 2006 15:04:05 MST).
|
||||||
|
func (t TimeRFC1123) MarshalBinary() ([]byte, error) {
|
||||||
|
return t.MarshalText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary reconstitutes a Time saved as a byte array conforming to RFC1123 date-time
|
||||||
|
// (i.e., Mon, 02 Jan 2006 15:04:05 MST).
|
||||||
|
func (t *TimeRFC1123) UnmarshalBinary(data []byte) error {
|
||||||
|
return t.UnmarshalText(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTime returns a Time as a time.Time
|
||||||
|
func (t TimeRFC1123) ToTime() time.Time {
|
||||||
|
return t.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the Time formatted as an RFC1123 date-time string (i.e.,
|
||||||
|
// Mon, 02 Jan 2006 15:04:05 MST).
|
||||||
|
func (t TimeRFC1123) String() string {
|
||||||
|
// Note: time.Time.String does not return an RFC1123 compliant string, time.Time.MarshalText does.
|
||||||
|
b, err := t.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
package date
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unixEpoch is the moment in time that should be treated as timestamp 0.
|
||||||
|
var unixEpoch = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// UnixTime marshals and unmarshals a time that is represented as the number
|
||||||
|
// of seconds (ignoring skip-seconds) since the Unix Epoch.
|
||||||
|
type UnixTime time.Time
|
||||||
|
|
||||||
|
// Duration returns the time as a Duration since the UnixEpoch.
|
||||||
|
func (t UnixTime) Duration() time.Duration {
|
||||||
|
return time.Time(t).Sub(unixEpoch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnixTimeFromSeconds creates a UnixTime as a number of seconds from the UnixEpoch.
|
||||||
|
func NewUnixTimeFromSeconds(seconds float64) UnixTime {
|
||||||
|
return NewUnixTimeFromDuration(time.Duration(seconds * float64(time.Second)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnixTimeFromNanoseconds creates a UnixTime as a number of nanoseconds from the UnixEpoch.
|
||||||
|
func NewUnixTimeFromNanoseconds(nanoseconds int64) UnixTime {
|
||||||
|
return NewUnixTimeFromDuration(time.Duration(nanoseconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUnixTimeFromDuration creates a UnixTime as a duration of time since the UnixEpoch.
|
||||||
|
func NewUnixTimeFromDuration(dur time.Duration) UnixTime {
|
||||||
|
return UnixTime(unixEpoch.Add(dur))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnixEpoch retreives the moment considered the Unix Epoch. I.e. The time represented by '0'
|
||||||
|
func UnixEpoch() time.Time {
|
||||||
|
return unixEpoch
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON preserves the UnixTime as a JSON number conforming to Unix Timestamp requirements.
|
||||||
|
// (i.e. the number of seconds since midnight January 1st, 1970 not considering leap seconds.)
|
||||||
|
func (t UnixTime) MarshalJSON() ([]byte, error) {
|
||||||
|
buffer := &bytes.Buffer{}
|
||||||
|
enc := json.NewEncoder(buffer)
|
||||||
|
err := enc.Encode(float64(time.Time(t).UnixNano()) / 1e9)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON reconstitures a UnixTime saved as a JSON number of the number of seconds since
|
||||||
|
// midnight January 1st, 1970.
|
||||||
|
func (t *UnixTime) UnmarshalJSON(text []byte) error {
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(text))
|
||||||
|
|
||||||
|
var secondsSinceEpoch float64
|
||||||
|
if err := dec.Decode(&secondsSinceEpoch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = NewUnixTimeFromSeconds(secondsSinceEpoch)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText stores the number of seconds since the Unix Epoch as a textual floating point number.
|
||||||
|
func (t UnixTime) MarshalText() ([]byte, error) {
|
||||||
|
cast := time.Time(t)
|
||||||
|
return cast.MarshalText()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText populates a UnixTime with a value stored textually as a floating point number of seconds since the Unix Epoch.
|
||||||
|
func (t *UnixTime) UnmarshalText(raw []byte) error {
|
||||||
|
var unmarshaled time.Time
|
||||||
|
|
||||||
|
if err := unmarshaled.UnmarshalText(raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = UnixTime(unmarshaled)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary converts a UnixTime into a binary.LittleEndian float64 of nanoseconds since the epoch.
|
||||||
|
func (t UnixTime) MarshalBinary() ([]byte, error) {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
payload := int64(t.Duration())
|
||||||
|
|
||||||
|
if err := binary.Write(buf, binary.LittleEndian, &payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary converts a from a binary.LittleEndian float64 of nanoseconds since the epoch into a UnixTime.
|
||||||
|
func (t *UnixTime) UnmarshalBinary(raw []byte) error {
|
||||||
|
var nanosecondsSinceEpoch int64
|
||||||
|
|
||||||
|
if err := binary.Read(bytes.NewReader(raw), binary.LittleEndian, &nanosecondsSinceEpoch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*t = NewUnixTimeFromNanoseconds(nanosecondsSinceEpoch)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package date
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseTime to parse Time string to specified format.
|
||||||
|
func ParseTime(format string, t string) (d time.Time, err error) {
|
||||||
|
return time.Parse(format, strings.ToUpper(t))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UndefinedStatusCode is used when HTTP status code is not available for an error.
|
||||||
|
UndefinedStatusCode = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetailedError encloses a error with details of the package, method, and associated HTTP
|
||||||
|
// status code (if any).
|
||||||
|
type DetailedError struct {
|
||||||
|
Original error
|
||||||
|
|
||||||
|
// PackageType is the package type of the object emitting the error. For types, the value
|
||||||
|
// matches that produced the the '%T' format specifier of the fmt package. For other elements,
|
||||||
|
// such as functions, it is just the package name (e.g., "autorest").
|
||||||
|
PackageType string
|
||||||
|
|
||||||
|
// Method is the name of the method raising the error.
|
||||||
|
Method string
|
||||||
|
|
||||||
|
// StatusCode is the HTTP Response StatusCode (if non-zero) that led to the error.
|
||||||
|
StatusCode interface{}
|
||||||
|
|
||||||
|
// Message is the error message.
|
||||||
|
Message string
|
||||||
|
|
||||||
|
// Service Error is the response body of failed API in bytes
|
||||||
|
ServiceError []byte
|
||||||
|
|
||||||
|
// Response is the response object that was returned during failure if applicable.
|
||||||
|
Response *http.Response
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError creates a new Error conforming object from the passed packageType, method, and
|
||||||
|
// message. message is treated as a format string to which the optional args apply.
|
||||||
|
func NewError(packageType string, method string, message string, args ...interface{}) DetailedError {
|
||||||
|
return NewErrorWithError(nil, packageType, method, nil, message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorWithResponse creates a new Error conforming object from the passed
|
||||||
|
// packageType, method, statusCode of the given resp (UndefinedStatusCode if
|
||||||
|
// resp is nil), and message. message is treated as a format string to which the
|
||||||
|
// optional args apply.
|
||||||
|
func NewErrorWithResponse(packageType string, method string, resp *http.Response, message string, args ...interface{}) DetailedError {
|
||||||
|
return NewErrorWithError(nil, packageType, method, resp, message, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorWithError creates a new Error conforming object from the
|
||||||
|
// passed packageType, method, statusCode of the given resp (UndefinedStatusCode
|
||||||
|
// if resp is nil), message, and original error. message is treated as a format
|
||||||
|
// string to which the optional args apply.
|
||||||
|
func NewErrorWithError(original error, packageType string, method string, resp *http.Response, message string, args ...interface{}) DetailedError {
|
||||||
|
if v, ok := original.(DetailedError); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode := UndefinedStatusCode
|
||||||
|
if resp != nil {
|
||||||
|
statusCode = resp.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
return DetailedError{
|
||||||
|
Original: original,
|
||||||
|
PackageType: packageType,
|
||||||
|
Method: method,
|
||||||
|
StatusCode: statusCode,
|
||||||
|
Message: fmt.Sprintf(message, args...),
|
||||||
|
Response: resp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a formatted containing all available details (i.e., PackageType, Method,
|
||||||
|
// StatusCode, Message, and original error (if any)).
|
||||||
|
func (e DetailedError) Error() string {
|
||||||
|
if e.Original == nil {
|
||||||
|
return fmt.Sprintf("%s#%s: %s: StatusCode=%d", e.PackageType, e.Method, e.Message, e.StatusCode)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s#%s: %s: StatusCode=%d -- Original Error: %v", e.PackageType, e.Method, e.Message, e.StatusCode, e.Original)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,480 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mimeTypeJSON = "application/json"
|
||||||
|
mimeTypeOctetStream = "application/octet-stream"
|
||||||
|
mimeTypeFormPost = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
|
headerAuthorization = "Authorization"
|
||||||
|
headerContentType = "Content-Type"
|
||||||
|
headerUserAgent = "User-Agent"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Preparer is the interface that wraps the Prepare method.
|
||||||
|
//
|
||||||
|
// Prepare accepts and possibly modifies an http.Request (e.g., adding Headers). Implementations
|
||||||
|
// must ensure to not share or hold per-invocation state since Preparers may be shared and re-used.
|
||||||
|
type Preparer interface {
|
||||||
|
Prepare(*http.Request) (*http.Request, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreparerFunc is a method that implements the Preparer interface.
|
||||||
|
type PreparerFunc func(*http.Request) (*http.Request, error)
|
||||||
|
|
||||||
|
// Prepare implements the Preparer interface on PreparerFunc.
|
||||||
|
func (pf PreparerFunc) Prepare(r *http.Request) (*http.Request, error) {
|
||||||
|
return pf(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareDecorator takes and possibly decorates, by wrapping, a Preparer. Decorators may affect the
|
||||||
|
// http.Request and pass it along or, first, pass the http.Request along then affect the result.
|
||||||
|
type PrepareDecorator func(Preparer) Preparer
|
||||||
|
|
||||||
|
// CreatePreparer creates, decorates, and returns a Preparer.
|
||||||
|
// Without decorators, the returned Preparer returns the passed http.Request unmodified.
|
||||||
|
// Preparers are safe to share and re-use.
|
||||||
|
func CreatePreparer(decorators ...PrepareDecorator) Preparer {
|
||||||
|
return DecoratePreparer(
|
||||||
|
Preparer(PreparerFunc(func(r *http.Request) (*http.Request, error) { return r, nil })),
|
||||||
|
decorators...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecoratePreparer accepts a Preparer and a, possibly empty, set of PrepareDecorators, which it
|
||||||
|
// applies to the Preparer. Decorators are applied in the order received, but their affect upon the
|
||||||
|
// request depends on whether they are a pre-decorator (change the http.Request and then pass it
|
||||||
|
// along) or a post-decorator (pass the http.Request along and alter it on return).
|
||||||
|
func DecoratePreparer(p Preparer, decorators ...PrepareDecorator) Preparer {
|
||||||
|
for _, decorate := range decorators {
|
||||||
|
p = decorate(p)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare accepts an http.Request and a, possibly empty, set of PrepareDecorators.
|
||||||
|
// It creates a Preparer from the decorators which it then applies to the passed http.Request.
|
||||||
|
func Prepare(r *http.Request, decorators ...PrepareDecorator) (*http.Request, error) {
|
||||||
|
if r == nil {
|
||||||
|
return nil, NewError("autorest", "Prepare", "Invoked without an http.Request")
|
||||||
|
}
|
||||||
|
return CreatePreparer(decorators...).Prepare(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithNothing returns a "do nothing" PrepareDecorator that makes no changes to the passed
|
||||||
|
// http.Request.
|
||||||
|
func WithNothing() PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
return p.Prepare(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeader returns a PrepareDecorator that sets the specified HTTP header of the http.Request to
|
||||||
|
// the passed value. It canonicalizes the passed header name (via http.CanonicalHeaderKey) before
|
||||||
|
// adding the header.
|
||||||
|
func WithHeader(header string, value string) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
if r.Header == nil {
|
||||||
|
r.Header = make(http.Header)
|
||||||
|
}
|
||||||
|
r.Header.Set(http.CanonicalHeaderKey(header), value)
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHeaders returns a PrepareDecorator that sets the specified HTTP headers of the http.Request to
|
||||||
|
// the passed value. It canonicalizes the passed headers name (via http.CanonicalHeaderKey) before
|
||||||
|
// adding them.
|
||||||
|
func WithHeaders(headers map[string]interface{}) PrepareDecorator {
|
||||||
|
h := ensureValueStrings(headers)
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
if r.Header == nil {
|
||||||
|
r.Header = make(http.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, value := range h {
|
||||||
|
r.Header.Set(http.CanonicalHeaderKey(name), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBearerAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
|
||||||
|
// value is "Bearer " followed by the supplied token.
|
||||||
|
func WithBearerAuthorization(token string) PrepareDecorator {
|
||||||
|
return WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", token))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsContentType returns a PrepareDecorator that adds an HTTP Content-Type header whose value
|
||||||
|
// is the passed contentType.
|
||||||
|
func AsContentType(contentType string) PrepareDecorator {
|
||||||
|
return WithHeader(headerContentType, contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUserAgent returns a PrepareDecorator that adds an HTTP User-Agent header whose value is the
|
||||||
|
// passed string.
|
||||||
|
func WithUserAgent(ua string) PrepareDecorator {
|
||||||
|
return WithHeader(headerUserAgent, ua)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsFormURLEncoded returns a PrepareDecorator that adds an HTTP Content-Type header whose value is
|
||||||
|
// "application/x-www-form-urlencoded".
|
||||||
|
func AsFormURLEncoded() PrepareDecorator {
|
||||||
|
return AsContentType(mimeTypeFormPost)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsJSON returns a PrepareDecorator that adds an HTTP Content-Type header whose value is
|
||||||
|
// "application/json".
|
||||||
|
func AsJSON() PrepareDecorator {
|
||||||
|
return AsContentType(mimeTypeJSON)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsOctetStream returns a PrepareDecorator that adds the "application/octet-stream" Content-Type header.
|
||||||
|
func AsOctetStream() PrepareDecorator {
|
||||||
|
return AsContentType(mimeTypeOctetStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMethod returns a PrepareDecorator that sets the HTTP method of the passed request. The
|
||||||
|
// decorator does not validate that the passed method string is a known HTTP method.
|
||||||
|
func WithMethod(method string) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r.Method = method
|
||||||
|
return p.Prepare(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsDelete returns a PrepareDecorator that sets the HTTP method to DELETE.
|
||||||
|
func AsDelete() PrepareDecorator { return WithMethod("DELETE") }
|
||||||
|
|
||||||
|
// AsGet returns a PrepareDecorator that sets the HTTP method to GET.
|
||||||
|
func AsGet() PrepareDecorator { return WithMethod("GET") }
|
||||||
|
|
||||||
|
// AsHead returns a PrepareDecorator that sets the HTTP method to HEAD.
|
||||||
|
func AsHead() PrepareDecorator { return WithMethod("HEAD") }
|
||||||
|
|
||||||
|
// AsOptions returns a PrepareDecorator that sets the HTTP method to OPTIONS.
|
||||||
|
func AsOptions() PrepareDecorator { return WithMethod("OPTIONS") }
|
||||||
|
|
||||||
|
// AsPatch returns a PrepareDecorator that sets the HTTP method to PATCH.
|
||||||
|
func AsPatch() PrepareDecorator { return WithMethod("PATCH") }
|
||||||
|
|
||||||
|
// AsPost returns a PrepareDecorator that sets the HTTP method to POST.
|
||||||
|
func AsPost() PrepareDecorator { return WithMethod("POST") }
|
||||||
|
|
||||||
|
// AsPut returns a PrepareDecorator that sets the HTTP method to PUT.
|
||||||
|
func AsPut() PrepareDecorator { return WithMethod("PUT") }
|
||||||
|
|
||||||
|
// WithBaseURL returns a PrepareDecorator that populates the http.Request with a url.URL constructed
|
||||||
|
// from the supplied baseUrl.
|
||||||
|
func WithBaseURL(baseURL string) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
var u *url.URL
|
||||||
|
if u, err = url.Parse(baseURL); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
if u.Scheme == "" {
|
||||||
|
err = fmt.Errorf("autorest: No scheme detected in URL %s", baseURL)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
r.URL = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCustomBaseURL returns a PrepareDecorator that replaces brace-enclosed keys within the
|
||||||
|
// request base URL (i.e., http.Request.URL) with the corresponding values from the passed map.
|
||||||
|
func WithCustomBaseURL(baseURL string, urlParameters map[string]interface{}) PrepareDecorator {
|
||||||
|
parameters := ensureValueStrings(urlParameters)
|
||||||
|
for key, value := range parameters {
|
||||||
|
baseURL = strings.Replace(baseURL, "{"+key+"}", value, -1)
|
||||||
|
}
|
||||||
|
return WithBaseURL(baseURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFormData returns a PrepareDecoratore that "URL encodes" (e.g., bar=baz&foo=quux) into the
|
||||||
|
// http.Request body.
|
||||||
|
func WithFormData(v url.Values) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
s := v.Encode()
|
||||||
|
|
||||||
|
if r.Header == nil {
|
||||||
|
r.Header = make(http.Header)
|
||||||
|
}
|
||||||
|
r.Header.Set(http.CanonicalHeaderKey(headerContentType), mimeTypeFormPost)
|
||||||
|
r.ContentLength = int64(len(s))
|
||||||
|
r.Body = ioutil.NopCloser(strings.NewReader(s))
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMultiPartFormData returns a PrepareDecoratore that "URL encodes" (e.g., bar=baz&foo=quux) form parameters
|
||||||
|
// into the http.Request body.
|
||||||
|
func WithMultiPartFormData(formDataParameters map[string]interface{}) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
for key, value := range formDataParameters {
|
||||||
|
if rc, ok := value.(io.ReadCloser); ok {
|
||||||
|
var fd io.Writer
|
||||||
|
if fd, err = writer.CreateFormFile(key, key); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
if _, err = io.Copy(fd, rc); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err = writer.WriteField(key, ensureValueString(value)); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = writer.Close(); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
if r.Header == nil {
|
||||||
|
r.Header = make(http.Header)
|
||||||
|
}
|
||||||
|
r.Header.Set(http.CanonicalHeaderKey(headerContentType), writer.FormDataContentType())
|
||||||
|
r.Body = ioutil.NopCloser(bytes.NewReader(body.Bytes()))
|
||||||
|
r.ContentLength = int64(body.Len())
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFile returns a PrepareDecorator that sends file in request body.
|
||||||
|
func WithFile(f io.ReadCloser) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
b, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
r.Body = ioutil.NopCloser(bytes.NewReader(b))
|
||||||
|
r.ContentLength = int64(len(b))
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBool returns a PrepareDecorator that encodes the passed bool into the body of the request
|
||||||
|
// and sets the Content-Length header.
|
||||||
|
func WithBool(v bool) PrepareDecorator {
|
||||||
|
return WithString(fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFloat32 returns a PrepareDecorator that encodes the passed float32 into the body of the
|
||||||
|
// request and sets the Content-Length header.
|
||||||
|
func WithFloat32(v float32) PrepareDecorator {
|
||||||
|
return WithString(fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFloat64 returns a PrepareDecorator that encodes the passed float64 into the body of the
|
||||||
|
// request and sets the Content-Length header.
|
||||||
|
func WithFloat64(v float64) PrepareDecorator {
|
||||||
|
return WithString(fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInt32 returns a PrepareDecorator that encodes the passed int32 into the body of the request
|
||||||
|
// and sets the Content-Length header.
|
||||||
|
func WithInt32(v int32) PrepareDecorator {
|
||||||
|
return WithString(fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInt64 returns a PrepareDecorator that encodes the passed int64 into the body of the request
|
||||||
|
// and sets the Content-Length header.
|
||||||
|
func WithInt64(v int64) PrepareDecorator {
|
||||||
|
return WithString(fmt.Sprintf("%v", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithString returns a PrepareDecorator that encodes the passed string into the body of the request
|
||||||
|
// and sets the Content-Length header.
|
||||||
|
func WithString(v string) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
r.ContentLength = int64(len(v))
|
||||||
|
r.Body = ioutil.NopCloser(strings.NewReader(v))
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithJSON returns a PrepareDecorator that encodes the data passed as JSON into the body of the
|
||||||
|
// request and sets the Content-Length header.
|
||||||
|
func WithJSON(v interface{}) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err == nil {
|
||||||
|
r.ContentLength = int64(len(b))
|
||||||
|
r.Body = ioutil.NopCloser(bytes.NewReader(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPath returns a PrepareDecorator that adds the supplied path to the request URL. If the path
|
||||||
|
// is absolute (that is, it begins with a "/"), it replaces the existing path.
|
||||||
|
func WithPath(path string) PrepareDecorator {
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
if r.URL == nil {
|
||||||
|
return r, NewError("autorest", "WithPath", "Invoked with a nil URL")
|
||||||
|
}
|
||||||
|
if r.URL, err = parseURL(r.URL, path); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEscapedPathParameters returns a PrepareDecorator that replaces brace-enclosed keys within the
|
||||||
|
// request path (i.e., http.Request.URL.Path) with the corresponding values from the passed map. The
|
||||||
|
// values will be escaped (aka URL encoded) before insertion into the path.
|
||||||
|
func WithEscapedPathParameters(path string, pathParameters map[string]interface{}) PrepareDecorator {
|
||||||
|
parameters := escapeValueStrings(ensureValueStrings(pathParameters))
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
if r.URL == nil {
|
||||||
|
return r, NewError("autorest", "WithEscapedPathParameters", "Invoked with a nil URL")
|
||||||
|
}
|
||||||
|
for key, value := range parameters {
|
||||||
|
path = strings.Replace(path, "{"+key+"}", value, -1)
|
||||||
|
}
|
||||||
|
if r.URL, err = parseURL(r.URL, path); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPathParameters returns a PrepareDecorator that replaces brace-enclosed keys within the
|
||||||
|
// request path (i.e., http.Request.URL.Path) with the corresponding values from the passed map.
|
||||||
|
func WithPathParameters(path string, pathParameters map[string]interface{}) PrepareDecorator {
|
||||||
|
parameters := ensureValueStrings(pathParameters)
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
if r.URL == nil {
|
||||||
|
return r, NewError("autorest", "WithPathParameters", "Invoked with a nil URL")
|
||||||
|
}
|
||||||
|
for key, value := range parameters {
|
||||||
|
path = strings.Replace(path, "{"+key+"}", value, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL, err = parseURL(r.URL, path); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseURL(u *url.URL, path string) (*url.URL, error) {
|
||||||
|
p := strings.TrimRight(u.String(), "/")
|
||||||
|
if !strings.HasPrefix(path, "/") {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
return url.Parse(p + path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithQueryParameters returns a PrepareDecorators that encodes and applies the query parameters
|
||||||
|
// given in the supplied map (i.e., key=value).
|
||||||
|
func WithQueryParameters(queryParameters map[string]interface{}) PrepareDecorator {
|
||||||
|
parameters := ensureValueStrings(queryParameters)
|
||||||
|
return func(p Preparer) Preparer {
|
||||||
|
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
|
||||||
|
r, err := p.Prepare(r)
|
||||||
|
if err == nil {
|
||||||
|
if r.URL == nil {
|
||||||
|
return r, NewError("autorest", "WithQueryParameters", "Invoked with a nil URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
v := r.URL.Query()
|
||||||
|
for key, value := range parameters {
|
||||||
|
d, err := url.QueryUnescape(value)
|
||||||
|
if err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
v.Add(key, d)
|
||||||
|
}
|
||||||
|
r.URL.RawQuery = v.Encode()
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,250 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Responder is the interface that wraps the Respond method.
|
||||||
|
//
|
||||||
|
// Respond accepts and reacts to an http.Response. Implementations must ensure to not share or hold
|
||||||
|
// state since Responders may be shared and re-used.
|
||||||
|
type Responder interface {
|
||||||
|
Respond(*http.Response) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponderFunc is a method that implements the Responder interface.
|
||||||
|
type ResponderFunc func(*http.Response) error
|
||||||
|
|
||||||
|
// Respond implements the Responder interface on ResponderFunc.
|
||||||
|
func (rf ResponderFunc) Respond(r *http.Response) error {
|
||||||
|
return rf(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondDecorator takes and possibly decorates, by wrapping, a Responder. Decorators may react to
|
||||||
|
// the http.Response and pass it along or, first, pass the http.Response along then react.
|
||||||
|
type RespondDecorator func(Responder) Responder
|
||||||
|
|
||||||
|
// CreateResponder creates, decorates, and returns a Responder. Without decorators, the returned
|
||||||
|
// Responder returns the passed http.Response unmodified. Responders may or may not be safe to share
|
||||||
|
// and re-used: It depends on the applied decorators. For example, a standard decorator that closes
|
||||||
|
// the response body is fine to share whereas a decorator that reads the body into a passed struct
|
||||||
|
// is not.
|
||||||
|
//
|
||||||
|
// To prevent memory leaks, ensure that at least one Responder closes the response body.
|
||||||
|
func CreateResponder(decorators ...RespondDecorator) Responder {
|
||||||
|
return DecorateResponder(
|
||||||
|
Responder(ResponderFunc(func(r *http.Response) error { return nil })),
|
||||||
|
decorators...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecorateResponder accepts a Responder and a, possibly empty, set of RespondDecorators, which it
|
||||||
|
// applies to the Responder. Decorators are applied in the order received, but their affect upon the
|
||||||
|
// request depends on whether they are a pre-decorator (react to the http.Response and then pass it
|
||||||
|
// along) or a post-decorator (pass the http.Response along and then react).
|
||||||
|
func DecorateResponder(r Responder, decorators ...RespondDecorator) Responder {
|
||||||
|
for _, decorate := range decorators {
|
||||||
|
r = decorate(r)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond accepts an http.Response and a, possibly empty, set of RespondDecorators.
|
||||||
|
// It creates a Responder from the decorators it then applies to the passed http.Response.
|
||||||
|
func Respond(r *http.Response, decorators ...RespondDecorator) error {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return CreateResponder(decorators...).Respond(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByIgnoring returns a RespondDecorator that ignores the passed http.Response passing it unexamined
|
||||||
|
// to the next RespondDecorator.
|
||||||
|
func ByIgnoring() RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
return r.Respond(resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByCopying copies the contents of the http.Response Body into the passed bytes.Buffer as
|
||||||
|
// the Body is read.
|
||||||
|
func ByCopying(b *bytes.Buffer) RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
err := r.Respond(resp)
|
||||||
|
if err == nil && resp != nil && resp.Body != nil {
|
||||||
|
resp.Body = TeeReadCloser(resp.Body, b)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByDiscardingBody returns a RespondDecorator that first invokes the passed Responder after which
|
||||||
|
// it copies the remaining bytes (if any) in the response body to ioutil.Discard. Since the passed
|
||||||
|
// Responder is invoked prior to discarding the response body, the decorator may occur anywhere
|
||||||
|
// within the set.
|
||||||
|
func ByDiscardingBody() RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
err := r.Respond(resp)
|
||||||
|
if err == nil && resp != nil && resp.Body != nil {
|
||||||
|
if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
|
||||||
|
return fmt.Errorf("Error discarding the response body: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByClosing returns a RespondDecorator that first invokes the passed Responder after which it
|
||||||
|
// closes the response body. Since the passed Responder is invoked prior to closing the response
|
||||||
|
// body, the decorator may occur anywhere within the set.
|
||||||
|
func ByClosing() RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
err := r.Respond(resp)
|
||||||
|
if resp != nil && resp.Body != nil {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
return fmt.Errorf("Error closing the response body: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByClosingIfError returns a RespondDecorator that first invokes the passed Responder after which
|
||||||
|
// it closes the response if the passed Responder returns an error and the response body exists.
|
||||||
|
func ByClosingIfError() RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
err := r.Respond(resp)
|
||||||
|
if err != nil && resp != nil && resp.Body != nil {
|
||||||
|
if err := resp.Body.Close(); err != nil {
|
||||||
|
return fmt.Errorf("Error closing the response body: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByUnmarshallingJSON returns a RespondDecorator that decodes a JSON document returned in the
|
||||||
|
// response Body into the value pointed to by v.
|
||||||
|
func ByUnmarshallingJSON(v interface{}) RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
err := r.Respond(resp)
|
||||||
|
if err == nil {
|
||||||
|
b, errInner := ioutil.ReadAll(resp.Body)
|
||||||
|
// Some responses might include a BOM, remove for successful unmarshalling
|
||||||
|
b = bytes.TrimPrefix(b, []byte("\xef\xbb\xbf"))
|
||||||
|
if errInner != nil {
|
||||||
|
err = fmt.Errorf("Error occurred reading http.Response#Body - Error = '%v'", errInner)
|
||||||
|
} else if len(strings.Trim(string(b), " ")) > 0 {
|
||||||
|
errInner = json.Unmarshal(b, v)
|
||||||
|
if errInner != nil {
|
||||||
|
err = fmt.Errorf("Error occurred unmarshalling JSON - Error = '%v' JSON = '%s'", errInner, string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByUnmarshallingXML returns a RespondDecorator that decodes a XML document returned in the
|
||||||
|
// response Body into the value pointed to by v.
|
||||||
|
func ByUnmarshallingXML(v interface{}) RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
err := r.Respond(resp)
|
||||||
|
if err == nil {
|
||||||
|
b, errInner := ioutil.ReadAll(resp.Body)
|
||||||
|
if errInner != nil {
|
||||||
|
err = fmt.Errorf("Error occurred reading http.Response#Body - Error = '%v'", errInner)
|
||||||
|
} else {
|
||||||
|
errInner = xml.Unmarshal(b, v)
|
||||||
|
if errInner != nil {
|
||||||
|
err = fmt.Errorf("Error occurred unmarshalling Xml - Error = '%v' Xml = '%s'", errInner, string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithErrorUnlessStatusCode returns a RespondDecorator that emits an error unless the response
|
||||||
|
// StatusCode is among the set passed. On error, response body is fully read into a buffer and
|
||||||
|
// presented in the returned error, as well as in the response body.
|
||||||
|
func WithErrorUnlessStatusCode(codes ...int) RespondDecorator {
|
||||||
|
return func(r Responder) Responder {
|
||||||
|
return ResponderFunc(func(resp *http.Response) error {
|
||||||
|
err := r.Respond(resp)
|
||||||
|
if err == nil && !ResponseHasStatusCode(resp, codes...) {
|
||||||
|
derr := NewErrorWithResponse("autorest", "WithErrorUnlessStatusCode", resp, "%v %v failed with %s",
|
||||||
|
resp.Request.Method,
|
||||||
|
resp.Request.URL,
|
||||||
|
resp.Status)
|
||||||
|
if resp.Body != nil {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
b, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
derr.ServiceError = b
|
||||||
|
resp.Body = ioutil.NopCloser(bytes.NewReader(b))
|
||||||
|
}
|
||||||
|
err = derr
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithErrorUnlessOK returns a RespondDecorator that emits an error if the response StatusCode is
|
||||||
|
// anything other than HTTP 200.
|
||||||
|
func WithErrorUnlessOK() RespondDecorator {
|
||||||
|
return WithErrorUnlessStatusCode(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractHeader extracts all values of the specified header from the http.Response. It returns an
|
||||||
|
// empty string slice if the passed http.Response is nil or the header does not exist.
|
||||||
|
func ExtractHeader(header string, resp *http.Response) []string {
|
||||||
|
if resp != nil && resp.Header != nil {
|
||||||
|
return resp.Header[http.CanonicalHeaderKey(header)]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractHeaderValue extracts the first value of the specified header from the http.Response. It
|
||||||
|
// returns an empty string if the passed http.Response is nil or the header does not exist.
|
||||||
|
func ExtractHeaderValue(header string, resp *http.Response) string {
|
||||||
|
h := ExtractHeader(header, resp)
|
||||||
|
if len(h) > 0 {
|
||||||
|
return h[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRetriableRequest returns a wrapper around an HTTP request that support retry logic.
|
||||||
|
func NewRetriableRequest(req *http.Request) *RetriableRequest {
|
||||||
|
return &RetriableRequest{req: req}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request returns the wrapped HTTP request.
|
||||||
|
func (rr *RetriableRequest) Request() *http.Request {
|
||||||
|
return rr.req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rr *RetriableRequest) prepareFromByteReader() (err error) {
|
||||||
|
// fall back to making a copy (only do this once)
|
||||||
|
b := []byte{}
|
||||||
|
if rr.req.ContentLength > 0 {
|
||||||
|
b = make([]byte, rr.req.ContentLength)
|
||||||
|
_, err = io.ReadFull(rr.req.Body, b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b, err = ioutil.ReadAll(rr.req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rr.br = bytes.NewReader(b)
|
||||||
|
rr.req.Body = ioutil.NopCloser(rr.br)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
// +build !go1.8
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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 autorest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RetriableRequest provides facilities for retrying an HTTP request.
|
||||||
|
type RetriableRequest struct {
|
||||||
|
req *http.Request
|
||||||
|
br *bytes.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare signals that the request is about to be sent.
|
||||||
|
func (rr *RetriableRequest) Prepare() (err error) {
|
||||||
|
// preserve the request body; this is to support retry logic as
|
||||||
|
// the underlying transport will always close the reqeust body
|
||||||
|
if rr.req.Body != nil {
|
||||||
|
if rr.br != nil {
|
||||||
|
_, err = rr.br.Seek(0, 0 /*io.SeekStart*/)
|
||||||
|
rr.req.Body = ioutil.NopCloser(rr.br)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rr.br == nil {
|
||||||
|
// fall back to making a copy (only do this once)
|
||||||
|
err = rr.prepareFromByteReader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeRequestBody(req *http.Request) {
|
||||||
|
req.Body = nil
|
||||||
|
req.ContentLength = 0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
// +build go1.8
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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 autorest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RetriableRequest provides facilities for retrying an HTTP request.
|
||||||
|
type RetriableRequest struct {
|
||||||
|
req *http.Request
|
||||||
|
rc io.ReadCloser
|
||||||
|
br *bytes.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare signals that the request is about to be sent.
|
||||||
|
func (rr *RetriableRequest) Prepare() (err error) {
|
||||||
|
// preserve the request body; this is to support retry logic as
|
||||||
|
// the underlying transport will always close the reqeust body
|
||||||
|
if rr.req.Body != nil {
|
||||||
|
if rr.rc != nil {
|
||||||
|
rr.req.Body = rr.rc
|
||||||
|
} else if rr.br != nil {
|
||||||
|
_, err = rr.br.Seek(0, io.SeekStart)
|
||||||
|
rr.req.Body = ioutil.NopCloser(rr.br)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rr.req.GetBody != nil {
|
||||||
|
// this will allow us to preserve the body without having to
|
||||||
|
// make a copy. note we need to do this on each iteration
|
||||||
|
rr.rc, err = rr.req.GetBody()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if rr.br == nil {
|
||||||
|
// fall back to making a copy (only do this once)
|
||||||
|
err = rr.prepareFromByteReader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeRequestBody(req *http.Request) {
|
||||||
|
req.Body = nil
|
||||||
|
req.GetBody = nil
|
||||||
|
req.ContentLength = 0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sender is the interface that wraps the Do method to send HTTP requests.
|
||||||
|
//
|
||||||
|
// The standard http.Client conforms to this interface.
|
||||||
|
type Sender interface {
|
||||||
|
Do(*http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SenderFunc is a method that implements the Sender interface.
|
||||||
|
type SenderFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
// Do implements the Sender interface on SenderFunc.
|
||||||
|
func (sf SenderFunc) Do(r *http.Request) (*http.Response, error) {
|
||||||
|
return sf(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendDecorator takes and possibility decorates, by wrapping, a Sender. Decorators may affect the
|
||||||
|
// http.Request and pass it along or, first, pass the http.Request along then react to the
|
||||||
|
// http.Response result.
|
||||||
|
type SendDecorator func(Sender) Sender
|
||||||
|
|
||||||
|
// CreateSender creates, decorates, and returns, as a Sender, the default http.Client.
|
||||||
|
func CreateSender(decorators ...SendDecorator) Sender {
|
||||||
|
return DecorateSender(&http.Client{}, decorators...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecorateSender accepts a Sender and a, possibly empty, set of SendDecorators, which is applies to
|
||||||
|
// the Sender. Decorators are applied in the order received, but their affect upon the request
|
||||||
|
// depends on whether they are a pre-decorator (change the http.Request and then pass it along) or a
|
||||||
|
// post-decorator (pass the http.Request along and react to the results in http.Response).
|
||||||
|
func DecorateSender(s Sender, decorators ...SendDecorator) Sender {
|
||||||
|
for _, decorate := range decorators {
|
||||||
|
s = decorate(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends, by means of the default http.Client, the passed http.Request, returning the
|
||||||
|
// http.Response and possible error. It also accepts a, possibly empty, set of SendDecorators which
|
||||||
|
// it will apply the http.Client before invoking the Do method.
|
||||||
|
//
|
||||||
|
// Send is a convenience method and not recommended for production. Advanced users should use
|
||||||
|
// SendWithSender, passing and sharing their own Sender (e.g., instance of http.Client).
|
||||||
|
//
|
||||||
|
// Send will not poll or retry requests.
|
||||||
|
func Send(r *http.Request, decorators ...SendDecorator) (*http.Response, error) {
|
||||||
|
return SendWithSender(&http.Client{}, r, decorators...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendWithSender sends the passed http.Request, through the provided Sender, returning the
|
||||||
|
// http.Response and possible error. It also accepts a, possibly empty, set of SendDecorators which
|
||||||
|
// it will apply the http.Client before invoking the Do method.
|
||||||
|
//
|
||||||
|
// SendWithSender will not poll or retry requests.
|
||||||
|
func SendWithSender(s Sender, r *http.Request, decorators ...SendDecorator) (*http.Response, error) {
|
||||||
|
return DecorateSender(s, decorators...).Do(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AfterDelay returns a SendDecorator that delays for the passed time.Duration before
|
||||||
|
// invoking the Sender. The delay may be terminated by closing the optional channel on the
|
||||||
|
// http.Request. If canceled, no further Senders are invoked.
|
||||||
|
func AfterDelay(d time.Duration) SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
if !DelayForBackoff(d, 0, r.Context().Done()) {
|
||||||
|
return nil, fmt.Errorf("autorest: AfterDelay canceled before full delay")
|
||||||
|
}
|
||||||
|
return s.Do(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsIs returns a SendDecorator that invokes the passed Sender without modifying the http.Request.
|
||||||
|
func AsIs() SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
return s.Do(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoCloseIfError returns a SendDecorator that first invokes the passed Sender after which
|
||||||
|
// it closes the response if the passed Sender returns an error and the response body exists.
|
||||||
|
func DoCloseIfError() SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
resp, err := s.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
Respond(resp, ByDiscardingBody(), ByClosing())
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoErrorIfStatusCode returns a SendDecorator that emits an error if the response StatusCode is
|
||||||
|
// among the set passed. Since these are artificial errors, the response body may still require
|
||||||
|
// closing.
|
||||||
|
func DoErrorIfStatusCode(codes ...int) SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
resp, err := s.Do(r)
|
||||||
|
if err == nil && ResponseHasStatusCode(resp, codes...) {
|
||||||
|
err = NewErrorWithResponse("autorest", "DoErrorIfStatusCode", resp, "%v %v failed with %s",
|
||||||
|
resp.Request.Method,
|
||||||
|
resp.Request.URL,
|
||||||
|
resp.Status)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoErrorUnlessStatusCode returns a SendDecorator that emits an error unless the response
|
||||||
|
// StatusCode is among the set passed. Since these are artificial errors, the response body
|
||||||
|
// may still require closing.
|
||||||
|
func DoErrorUnlessStatusCode(codes ...int) SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
resp, err := s.Do(r)
|
||||||
|
if err == nil && !ResponseHasStatusCode(resp, codes...) {
|
||||||
|
err = NewErrorWithResponse("autorest", "DoErrorUnlessStatusCode", resp, "%v %v failed with %s",
|
||||||
|
resp.Request.Method,
|
||||||
|
resp.Request.URL,
|
||||||
|
resp.Status)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoPollForStatusCodes returns a SendDecorator that polls if the http.Response contains one of the
|
||||||
|
// passed status codes. It expects the http.Response to contain a Location header providing the
|
||||||
|
// URL at which to poll (using GET) and will poll until the time passed is equal to or greater than
|
||||||
|
// the supplied duration. It will delay between requests for the duration specified in the
|
||||||
|
// RetryAfter header or, if the header is absent, the passed delay. Polling may be canceled by
|
||||||
|
// closing the optional channel on the http.Request.
|
||||||
|
func DoPollForStatusCodes(duration time.Duration, delay time.Duration, codes ...int) SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
||||||
|
resp, err = s.Do(r)
|
||||||
|
|
||||||
|
if err == nil && ResponseHasStatusCode(resp, codes...) {
|
||||||
|
r, err = NewPollingRequestWithContext(r.Context(), resp)
|
||||||
|
|
||||||
|
for err == nil && ResponseHasStatusCode(resp, codes...) {
|
||||||
|
Respond(resp,
|
||||||
|
ByDiscardingBody(),
|
||||||
|
ByClosing())
|
||||||
|
resp, err = SendWithSender(s, r,
|
||||||
|
AfterDelay(GetRetryAfter(resp, delay)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoRetryForAttempts returns a SendDecorator that retries a failed request for up to the specified
|
||||||
|
// number of attempts, exponentially backing off between requests using the supplied backoff
|
||||||
|
// time.Duration (which may be zero). Retrying may be canceled by closing the optional channel on
|
||||||
|
// the http.Request.
|
||||||
|
func DoRetryForAttempts(attempts int, backoff time.Duration) SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
||||||
|
rr := NewRetriableRequest(r)
|
||||||
|
for attempt := 0; attempt < attempts; attempt++ {
|
||||||
|
err = rr.Prepare()
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
resp, err = s.Do(rr.Request())
|
||||||
|
if err == nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if !DelayForBackoff(backoff, attempt, r.Context().Done()) {
|
||||||
|
return nil, r.Context().Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoRetryForStatusCodes returns a SendDecorator that retries for specified statusCodes for up to the specified
|
||||||
|
// number of attempts, exponentially backing off between requests using the supplied backoff
|
||||||
|
// time.Duration (which may be zero). Retrying may be canceled by closing the optional channel on
|
||||||
|
// the http.Request.
|
||||||
|
func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
||||||
|
rr := NewRetriableRequest(r)
|
||||||
|
// Increment to add the first call (attempts denotes number of retries)
|
||||||
|
attempts++
|
||||||
|
for attempt := 0; attempt < attempts; {
|
||||||
|
err = rr.Prepare()
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
resp, err = s.Do(rr.Request())
|
||||||
|
// if the error isn't temporary don't bother retrying
|
||||||
|
if err != nil && !IsTemporaryNetworkError(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// we want to retry if err is not nil (e.g. transient network failure). note that for failed authentication
|
||||||
|
// resp and err will both have a value, so in this case we don't want to retry as it will never succeed.
|
||||||
|
if err == nil && !ResponseHasStatusCode(resp, codes...) || IsTokenRefreshError(err) {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
delayed := DelayWithRetryAfter(resp, r.Context().Done())
|
||||||
|
if !delayed && !DelayForBackoff(backoff, attempt, r.Context().Done()) {
|
||||||
|
return nil, r.Context().Err()
|
||||||
|
}
|
||||||
|
// don't count a 429 against the number of attempts
|
||||||
|
// so that we continue to retry until it succeeds
|
||||||
|
if resp == nil || resp.StatusCode != http.StatusTooManyRequests {
|
||||||
|
attempt++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header in
|
||||||
|
// responses with status code 429
|
||||||
|
func DelayWithRetryAfter(resp *http.Response, cancel <-chan struct{}) bool {
|
||||||
|
if resp == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
|
||||||
|
if resp.StatusCode == http.StatusTooManyRequests && retryAfter > 0 {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Duration(retryAfter) * time.Second):
|
||||||
|
return true
|
||||||
|
case <-cancel:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoRetryForDuration returns a SendDecorator that retries the request until the total time is equal
|
||||||
|
// to or greater than the specified duration, exponentially backing off between requests using the
|
||||||
|
// supplied backoff time.Duration (which may be zero). Retrying may be canceled by closing the
|
||||||
|
// optional channel on the http.Request.
|
||||||
|
func DoRetryForDuration(d time.Duration, backoff time.Duration) SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
|
||||||
|
rr := NewRetriableRequest(r)
|
||||||
|
end := time.Now().Add(d)
|
||||||
|
for attempt := 0; time.Now().Before(end); attempt++ {
|
||||||
|
err = rr.Prepare()
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
resp, err = s.Do(rr.Request())
|
||||||
|
if err == nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
if !DelayForBackoff(backoff, attempt, r.Context().Done()) {
|
||||||
|
return nil, r.Context().Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogging returns a SendDecorator that implements simple before and after logging of the
|
||||||
|
// request.
|
||||||
|
func WithLogging(logger *log.Logger) SendDecorator {
|
||||||
|
return func(s Sender) Sender {
|
||||||
|
return SenderFunc(func(r *http.Request) (*http.Response, error) {
|
||||||
|
logger.Printf("Sending %s %s", r.Method, r.URL)
|
||||||
|
resp, err := s.Do(r)
|
||||||
|
if err != nil {
|
||||||
|
logger.Printf("%s %s received error '%v'", r.Method, r.URL, err)
|
||||||
|
} else {
|
||||||
|
logger.Printf("%s %s received %s", r.Method, r.URL, resp.Status)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelayForBackoff invokes time.After for the supplied backoff duration raised to the power of
|
||||||
|
// passed attempt (i.e., an exponential backoff delay). Backoff duration is in seconds and can set
|
||||||
|
// to zero for no delay. The delay may be canceled by closing the passed channel. If terminated early,
|
||||||
|
// returns false.
|
||||||
|
// Note: Passing attempt 1 will result in doubling "backoff" duration. Treat this as a zero-based attempt
|
||||||
|
// count.
|
||||||
|
func DelayForBackoff(backoff time.Duration, attempt int, cancel <-chan struct{}) bool {
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Duration(backoff.Seconds()*math.Pow(2, float64(attempt))) * time.Second):
|
||||||
|
return true
|
||||||
|
case <-cancel:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
Package to provides helpers to ease working with pointer values of marshalled structures.
|
||||||
|
*/
|
||||||
|
package to
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// String returns a string value for the passed string pointer. It returns the empty string if the
|
||||||
|
// pointer is nil.
|
||||||
|
func String(s *string) string {
|
||||||
|
if s != nil {
|
||||||
|
return *s
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringPtr returns a pointer to the passed string.
|
||||||
|
func StringPtr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlice returns a string slice value for the passed string slice pointer. It returns a nil
|
||||||
|
// slice if the pointer is nil.
|
||||||
|
func StringSlice(s *[]string) []string {
|
||||||
|
if s != nil {
|
||||||
|
return *s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringSlicePtr returns a pointer to the passed string slice.
|
||||||
|
func StringSlicePtr(s []string) *[]string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringMap returns a map of strings built from the map of string pointers. The empty string is
|
||||||
|
// used for nil pointers.
|
||||||
|
func StringMap(msp map[string]*string) map[string]string {
|
||||||
|
ms := make(map[string]string, len(msp))
|
||||||
|
for k, sp := range msp {
|
||||||
|
if sp != nil {
|
||||||
|
ms[k] = *sp
|
||||||
|
} else {
|
||||||
|
ms[k] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringMapPtr returns a pointer to a map of string pointers built from the passed map of strings.
|
||||||
|
func StringMapPtr(ms map[string]string) *map[string]*string {
|
||||||
|
msp := make(map[string]*string, len(ms))
|
||||||
|
for k, s := range ms {
|
||||||
|
msp[k] = StringPtr(s)
|
||||||
|
}
|
||||||
|
return &msp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool returns a bool value for the passed bool pointer. It returns false if the pointer is nil.
|
||||||
|
func Bool(b *bool) bool {
|
||||||
|
if b != nil {
|
||||||
|
return *b
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoolPtr returns a pointer to the passed bool.
|
||||||
|
func BoolPtr(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns an int value for the passed int pointer. It returns 0 if the pointer is nil.
|
||||||
|
func Int(i *int) int {
|
||||||
|
if i != nil {
|
||||||
|
return *i
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntPtr returns a pointer to the passed int.
|
||||||
|
func IntPtr(i int) *int {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int32 returns an int value for the passed int pointer. It returns 0 if the pointer is nil.
|
||||||
|
func Int32(i *int32) int32 {
|
||||||
|
if i != nil {
|
||||||
|
return *i
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int32Ptr returns a pointer to the passed int32.
|
||||||
|
func Int32Ptr(i int32) *int32 {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 returns an int value for the passed int pointer. It returns 0 if the pointer is nil.
|
||||||
|
func Int64(i *int64) int64 {
|
||||||
|
if i != nil {
|
||||||
|
return *i
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64Ptr returns a pointer to the passed int64.
|
||||||
|
func Int64Ptr(i int64) *int64 {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float32 returns an int value for the passed int pointer. It returns 0.0 if the pointer is nil.
|
||||||
|
func Float32(i *float32) float32 {
|
||||||
|
if i != nil {
|
||||||
|
return *i
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float32Ptr returns a pointer to the passed float32.
|
||||||
|
func Float32Ptr(i float32) *float32 {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 returns an int value for the passed int pointer. It returns 0.0 if the pointer is nil.
|
||||||
|
func Float64(i *float64) float64 {
|
||||||
|
if i != nil {
|
||||||
|
return *i
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64Ptr returns a pointer to the passed float64.
|
||||||
|
func Float64Ptr(i float64) *float64 {
|
||||||
|
return &i
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,228 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Azure/go-autorest/autorest/adal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodedAs is a series of constants specifying various data encodings
|
||||||
|
type EncodedAs string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EncodedAsJSON states that data is encoded as JSON
|
||||||
|
EncodedAsJSON EncodedAs = "JSON"
|
||||||
|
|
||||||
|
// EncodedAsXML states that data is encoded as Xml
|
||||||
|
EncodedAsXML EncodedAs = "XML"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decoder defines the decoding method json.Decoder and xml.Decoder share
|
||||||
|
type Decoder interface {
|
||||||
|
Decode(v interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDecoder creates a new decoder appropriate to the passed encoding.
|
||||||
|
// encodedAs specifies the type of encoding and r supplies the io.Reader containing the
|
||||||
|
// encoded data.
|
||||||
|
func NewDecoder(encodedAs EncodedAs, r io.Reader) Decoder {
|
||||||
|
if encodedAs == EncodedAsJSON {
|
||||||
|
return json.NewDecoder(r)
|
||||||
|
} else if encodedAs == EncodedAsXML {
|
||||||
|
return xml.NewDecoder(r)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyAndDecode decodes the data from the passed io.Reader while making a copy. Having a copy
|
||||||
|
// is especially useful if there is a chance the data will fail to decode.
|
||||||
|
// encodedAs specifies the expected encoding, r provides the io.Reader to the data, and v
|
||||||
|
// is the decoding destination.
|
||||||
|
func CopyAndDecode(encodedAs EncodedAs, r io.Reader, v interface{}) (bytes.Buffer, error) {
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
return b, NewDecoder(encodedAs, io.TeeReader(r, &b)).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TeeReadCloser returns a ReadCloser that writes to w what it reads from rc.
|
||||||
|
// It utilizes io.TeeReader to copy the data read and has the same behavior when reading.
|
||||||
|
// Further, when it is closed, it ensures that rc is closed as well.
|
||||||
|
func TeeReadCloser(rc io.ReadCloser, w io.Writer) io.ReadCloser {
|
||||||
|
return &teeReadCloser{rc, io.TeeReader(rc, w)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type teeReadCloser struct {
|
||||||
|
rc io.ReadCloser
|
||||||
|
r io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *teeReadCloser) Read(p []byte) (int, error) {
|
||||||
|
return t.r.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *teeReadCloser) Close() error {
|
||||||
|
return t.rc.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsInt(ints []int, n int) bool {
|
||||||
|
for _, i := range ints {
|
||||||
|
if i == n {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeValueStrings(m map[string]string) map[string]string {
|
||||||
|
for key, value := range m {
|
||||||
|
m[key] = url.QueryEscape(value)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureValueStrings(mapOfInterface map[string]interface{}) map[string]string {
|
||||||
|
mapOfStrings := make(map[string]string)
|
||||||
|
for key, value := range mapOfInterface {
|
||||||
|
mapOfStrings[key] = ensureValueString(value)
|
||||||
|
}
|
||||||
|
return mapOfStrings
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureValueString(value interface{}) string {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch v := value.(type) {
|
||||||
|
case string:
|
||||||
|
return v
|
||||||
|
case []byte:
|
||||||
|
return string(v)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapToValues method converts map[string]interface{} to url.Values.
|
||||||
|
func MapToValues(m map[string]interface{}) url.Values {
|
||||||
|
v := url.Values{}
|
||||||
|
for key, value := range m {
|
||||||
|
x := reflect.ValueOf(value)
|
||||||
|
if x.Kind() == reflect.Array || x.Kind() == reflect.Slice {
|
||||||
|
for i := 0; i < x.Len(); i++ {
|
||||||
|
v.Add(key, ensureValueString(x.Index(i)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
v.Add(key, ensureValueString(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// AsStringSlice method converts interface{} to []string. This expects a
|
||||||
|
//that the parameter passed to be a slice or array of a type that has the underlying
|
||||||
|
//type a string.
|
||||||
|
func AsStringSlice(s interface{}) ([]string, error) {
|
||||||
|
v := reflect.ValueOf(s)
|
||||||
|
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
|
||||||
|
return nil, NewError("autorest", "AsStringSlice", "the value's type is not an array.")
|
||||||
|
}
|
||||||
|
stringSlice := make([]string, 0, v.Len())
|
||||||
|
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
stringSlice = append(stringSlice, v.Index(i).String())
|
||||||
|
}
|
||||||
|
return stringSlice, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String method converts interface v to string. If interface is a list, it
|
||||||
|
// joins list elements using the separator. Note that only sep[0] will be used for
|
||||||
|
// joining if any separator is specified.
|
||||||
|
func String(v interface{}, sep ...string) string {
|
||||||
|
if len(sep) == 0 {
|
||||||
|
return ensureValueString(v)
|
||||||
|
}
|
||||||
|
stringSlice, ok := v.([]string)
|
||||||
|
if ok == false {
|
||||||
|
var err error
|
||||||
|
stringSlice, err = AsStringSlice(v)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("autorest: Couldn't convert value to a string %s.", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ensureValueString(strings.Join(stringSlice, sep[0]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode method encodes url path and query parameters.
|
||||||
|
func Encode(location string, v interface{}, sep ...string) string {
|
||||||
|
s := String(v, sep...)
|
||||||
|
switch strings.ToLower(location) {
|
||||||
|
case "path":
|
||||||
|
return pathEscape(s)
|
||||||
|
case "query":
|
||||||
|
return queryEscape(s)
|
||||||
|
default:
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathEscape(s string) string {
|
||||||
|
return strings.Replace(url.QueryEscape(s), "+", "%20", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryEscape(s string) string {
|
||||||
|
return url.QueryEscape(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChangeToGet turns the specified http.Request into a GET (it assumes it wasn't).
|
||||||
|
// This is mainly useful for long-running operations that use the Azure-AsyncOperation
|
||||||
|
// header, so we change the initial PUT into a GET to retrieve the final result.
|
||||||
|
func ChangeToGet(req *http.Request) *http.Request {
|
||||||
|
req.Method = "GET"
|
||||||
|
req.Body = nil
|
||||||
|
req.ContentLength = 0
|
||||||
|
req.Header.Del("Content-Length")
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTokenRefreshError returns true if the specified error implements the TokenRefreshError
|
||||||
|
// interface. If err is a DetailedError it will walk the chain of Original errors.
|
||||||
|
func IsTokenRefreshError(err error) bool {
|
||||||
|
if _, ok := err.(adal.TokenRefreshError); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if de, ok := err.(DetailedError); ok {
|
||||||
|
return IsTokenRefreshError(de.Original)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTemporaryNetworkError returns true if the specified error is a temporary network error or false
|
||||||
|
// if it's not. If the error doesn't implement the net.Error interface the return value is true.
|
||||||
|
func IsTemporaryNetworkError(err error) bool {
|
||||||
|
if netErr, ok := err.(net.Error); !ok || (ok && netErr.Temporary()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package validation
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error is the type that's returned when the validation of an APIs arguments constraints fails.
|
||||||
|
type Error struct {
|
||||||
|
// PackageType is the package type of the object emitting the error. For types, the value
|
||||||
|
// matches that produced the the '%T' format specifier of the fmt package. For other elements,
|
||||||
|
// such as functions, it is just the package name (e.g., "autorest").
|
||||||
|
PackageType string
|
||||||
|
|
||||||
|
// Method is the name of the method raising the error.
|
||||||
|
Method string
|
||||||
|
|
||||||
|
// Message is the error message.
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a string containing the details of the validation failure.
|
||||||
|
func (e Error) Error() string {
|
||||||
|
return fmt.Sprintf("%s#%s: Invalid input: %s", e.PackageType, e.Method, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewError creates a new Error object with the specified parameters.
|
||||||
|
// message is treated as a format string to which the optional args apply.
|
||||||
|
func NewError(packageType string, method string, message string, args ...interface{}) Error {
|
||||||
|
return Error{
|
||||||
|
PackageType: packageType,
|
||||||
|
Method: method,
|
||||||
|
Message: fmt.Sprintf(message, args...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,408 @@
|
||||||
|
/*
|
||||||
|
Package validation provides methods for validating parameter value using reflection.
|
||||||
|
*/
|
||||||
|
package validation
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constraint stores constraint name, target field name
|
||||||
|
// Rule and chain validations.
|
||||||
|
type Constraint struct {
|
||||||
|
|
||||||
|
// Target field name for validation.
|
||||||
|
Target string
|
||||||
|
|
||||||
|
// Constraint name e.g. minLength, MaxLength, Pattern, etc.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Rule for constraint e.g. greater than 10, less than 5 etc.
|
||||||
|
Rule interface{}
|
||||||
|
|
||||||
|
// Chain Validations for struct type
|
||||||
|
Chain []Constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation stores parameter-wise validation.
|
||||||
|
type Validation struct {
|
||||||
|
TargetValue interface{}
|
||||||
|
Constraints []Constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constraint list
|
||||||
|
const (
|
||||||
|
Empty = "Empty"
|
||||||
|
Null = "Null"
|
||||||
|
ReadOnly = "ReadOnly"
|
||||||
|
Pattern = "Pattern"
|
||||||
|
MaxLength = "MaxLength"
|
||||||
|
MinLength = "MinLength"
|
||||||
|
MaxItems = "MaxItems"
|
||||||
|
MinItems = "MinItems"
|
||||||
|
MultipleOf = "MultipleOf"
|
||||||
|
UniqueItems = "UniqueItems"
|
||||||
|
InclusiveMaximum = "InclusiveMaximum"
|
||||||
|
ExclusiveMaximum = "ExclusiveMaximum"
|
||||||
|
ExclusiveMinimum = "ExclusiveMinimum"
|
||||||
|
InclusiveMinimum = "InclusiveMinimum"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate method validates constraints on parameter
|
||||||
|
// passed in validation array.
|
||||||
|
func Validate(m []Validation) error {
|
||||||
|
for _, item := range m {
|
||||||
|
v := reflect.ValueOf(item.TargetValue)
|
||||||
|
for _, constraint := range item.Constraints {
|
||||||
|
var err error
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
err = validatePtr(v, constraint)
|
||||||
|
case reflect.String:
|
||||||
|
err = validateString(v, constraint)
|
||||||
|
case reflect.Struct:
|
||||||
|
err = validateStruct(v, constraint)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
err = validateInt(v, constraint)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
err = validateFloat(v, constraint)
|
||||||
|
case reflect.Array, reflect.Slice, reflect.Map:
|
||||||
|
err = validateArrayMap(v, constraint)
|
||||||
|
default:
|
||||||
|
err = createError(v, constraint, fmt.Sprintf("unknown type %v", v.Kind()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateStruct(x reflect.Value, v Constraint, name ...string) error {
|
||||||
|
//Get field name from target name which is in format a.b.c
|
||||||
|
s := strings.Split(v.Target, ".")
|
||||||
|
f := x.FieldByName(s[len(s)-1])
|
||||||
|
if isZero(f) {
|
||||||
|
return createError(x, v, fmt.Sprintf("field %q doesn't exist", v.Target))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Validate([]Validation{
|
||||||
|
{
|
||||||
|
TargetValue: getInterfaceValue(f),
|
||||||
|
Constraints: []Constraint{v},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePtr(x reflect.Value, v Constraint) error {
|
||||||
|
if v.Name == ReadOnly {
|
||||||
|
if !x.IsNil() {
|
||||||
|
return createError(x.Elem(), v, "readonly parameter; must send as nil or empty in request")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if x.IsNil() {
|
||||||
|
return checkNil(x, v)
|
||||||
|
}
|
||||||
|
if v.Chain != nil {
|
||||||
|
return Validate([]Validation{
|
||||||
|
{
|
||||||
|
TargetValue: getInterfaceValue(x.Elem()),
|
||||||
|
Constraints: v.Chain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateInt(x reflect.Value, v Constraint) error {
|
||||||
|
i := x.Int()
|
||||||
|
r, ok := toInt64(v.Rule)
|
||||||
|
if !ok {
|
||||||
|
return createError(x, v, fmt.Sprintf("rule must be integer value for %v constraint; got: %v", v.Name, v.Rule))
|
||||||
|
}
|
||||||
|
switch v.Name {
|
||||||
|
case MultipleOf:
|
||||||
|
if i%r != 0 {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be a multiple of %v", r))
|
||||||
|
}
|
||||||
|
case ExclusiveMinimum:
|
||||||
|
if i <= r {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be greater than %v", r))
|
||||||
|
}
|
||||||
|
case ExclusiveMaximum:
|
||||||
|
if i >= r {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be less than %v", r))
|
||||||
|
}
|
||||||
|
case InclusiveMinimum:
|
||||||
|
if i < r {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be greater than or equal to %v", r))
|
||||||
|
}
|
||||||
|
case InclusiveMaximum:
|
||||||
|
if i > r {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be less than or equal to %v", r))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return createError(x, v, fmt.Sprintf("constraint %v is not applicable for type integer", v.Name))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFloat(x reflect.Value, v Constraint) error {
|
||||||
|
f := x.Float()
|
||||||
|
r, ok := v.Rule.(float64)
|
||||||
|
if !ok {
|
||||||
|
return createError(x, v, fmt.Sprintf("rule must be float value for %v constraint; got: %v", v.Name, v.Rule))
|
||||||
|
}
|
||||||
|
switch v.Name {
|
||||||
|
case ExclusiveMinimum:
|
||||||
|
if f <= r {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be greater than %v", r))
|
||||||
|
}
|
||||||
|
case ExclusiveMaximum:
|
||||||
|
if f >= r {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be less than %v", r))
|
||||||
|
}
|
||||||
|
case InclusiveMinimum:
|
||||||
|
if f < r {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be greater than or equal to %v", r))
|
||||||
|
}
|
||||||
|
case InclusiveMaximum:
|
||||||
|
if f > r {
|
||||||
|
return createError(x, v, fmt.Sprintf("value must be less than or equal to %v", r))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return createError(x, v, fmt.Sprintf("constraint %s is not applicable for type float", v.Name))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateString(x reflect.Value, v Constraint) error {
|
||||||
|
s := x.String()
|
||||||
|
switch v.Name {
|
||||||
|
case Empty:
|
||||||
|
if len(s) == 0 {
|
||||||
|
return checkEmpty(x, v)
|
||||||
|
}
|
||||||
|
case Pattern:
|
||||||
|
reg, err := regexp.Compile(v.Rule.(string))
|
||||||
|
if err != nil {
|
||||||
|
return createError(x, v, err.Error())
|
||||||
|
}
|
||||||
|
if !reg.MatchString(s) {
|
||||||
|
return createError(x, v, fmt.Sprintf("value doesn't match pattern %v", v.Rule))
|
||||||
|
}
|
||||||
|
case MaxLength:
|
||||||
|
if _, ok := v.Rule.(int); !ok {
|
||||||
|
return createError(x, v, fmt.Sprintf("rule must be integer value for %v constraint; got: %v", v.Name, v.Rule))
|
||||||
|
}
|
||||||
|
if len(s) > v.Rule.(int) {
|
||||||
|
return createError(x, v, fmt.Sprintf("value length must be less than or equal to %v", v.Rule))
|
||||||
|
}
|
||||||
|
case MinLength:
|
||||||
|
if _, ok := v.Rule.(int); !ok {
|
||||||
|
return createError(x, v, fmt.Sprintf("rule must be integer value for %v constraint; got: %v", v.Name, v.Rule))
|
||||||
|
}
|
||||||
|
if len(s) < v.Rule.(int) {
|
||||||
|
return createError(x, v, fmt.Sprintf("value length must be greater than or equal to %v", v.Rule))
|
||||||
|
}
|
||||||
|
case ReadOnly:
|
||||||
|
if len(s) > 0 {
|
||||||
|
return createError(reflect.ValueOf(s), v, "readonly parameter; must send as nil or empty in request")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return createError(x, v, fmt.Sprintf("constraint %s is not applicable to string type", v.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Chain != nil {
|
||||||
|
return Validate([]Validation{
|
||||||
|
{
|
||||||
|
TargetValue: getInterfaceValue(x),
|
||||||
|
Constraints: v.Chain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateArrayMap(x reflect.Value, v Constraint) error {
|
||||||
|
switch v.Name {
|
||||||
|
case Null:
|
||||||
|
if x.IsNil() {
|
||||||
|
return checkNil(x, v)
|
||||||
|
}
|
||||||
|
case Empty:
|
||||||
|
if x.IsNil() || x.Len() == 0 {
|
||||||
|
return checkEmpty(x, v)
|
||||||
|
}
|
||||||
|
case MaxItems:
|
||||||
|
if _, ok := v.Rule.(int); !ok {
|
||||||
|
return createError(x, v, fmt.Sprintf("rule must be integer for %v constraint; got: %v", v.Name, v.Rule))
|
||||||
|
}
|
||||||
|
if x.Len() > v.Rule.(int) {
|
||||||
|
return createError(x, v, fmt.Sprintf("maximum item limit is %v; got: %v", v.Rule, x.Len()))
|
||||||
|
}
|
||||||
|
case MinItems:
|
||||||
|
if _, ok := v.Rule.(int); !ok {
|
||||||
|
return createError(x, v, fmt.Sprintf("rule must be integer for %v constraint; got: %v", v.Name, v.Rule))
|
||||||
|
}
|
||||||
|
if x.Len() < v.Rule.(int) {
|
||||||
|
return createError(x, v, fmt.Sprintf("minimum item limit is %v; got: %v", v.Rule, x.Len()))
|
||||||
|
}
|
||||||
|
case UniqueItems:
|
||||||
|
if x.Kind() == reflect.Array || x.Kind() == reflect.Slice {
|
||||||
|
if !checkForUniqueInArray(x) {
|
||||||
|
return createError(x, v, fmt.Sprintf("all items in parameter %q must be unique; got:%v", v.Target, x))
|
||||||
|
}
|
||||||
|
} else if x.Kind() == reflect.Map {
|
||||||
|
if !checkForUniqueInMap(x) {
|
||||||
|
return createError(x, v, fmt.Sprintf("all items in parameter %q must be unique; got:%v", v.Target, x))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return createError(x, v, fmt.Sprintf("type must be array, slice or map for constraint %v; got: %v", v.Name, x.Kind()))
|
||||||
|
}
|
||||||
|
case ReadOnly:
|
||||||
|
if x.Len() != 0 {
|
||||||
|
return createError(x, v, "readonly parameter; must send as nil or empty in request")
|
||||||
|
}
|
||||||
|
case Pattern:
|
||||||
|
reg, err := regexp.Compile(v.Rule.(string))
|
||||||
|
if err != nil {
|
||||||
|
return createError(x, v, err.Error())
|
||||||
|
}
|
||||||
|
keys := x.MapKeys()
|
||||||
|
for _, k := range keys {
|
||||||
|
if !reg.MatchString(k.String()) {
|
||||||
|
return createError(k, v, fmt.Sprintf("map key doesn't match pattern %v", v.Rule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return createError(x, v, fmt.Sprintf("constraint %v is not applicable to array, slice and map type", v.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Chain != nil {
|
||||||
|
return Validate([]Validation{
|
||||||
|
{
|
||||||
|
TargetValue: getInterfaceValue(x),
|
||||||
|
Constraints: v.Chain,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNil(x reflect.Value, v Constraint) error {
|
||||||
|
if _, ok := v.Rule.(bool); !ok {
|
||||||
|
return createError(x, v, fmt.Sprintf("rule must be bool value for %v constraint; got: %v", v.Name, v.Rule))
|
||||||
|
}
|
||||||
|
if v.Rule.(bool) {
|
||||||
|
return createError(x, v, "value can not be null; required parameter")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkEmpty(x reflect.Value, v Constraint) error {
|
||||||
|
if _, ok := v.Rule.(bool); !ok {
|
||||||
|
return createError(x, v, fmt.Sprintf("rule must be bool value for %v constraint; got: %v", v.Name, v.Rule))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Rule.(bool) {
|
||||||
|
return createError(x, v, "value can not be null or empty; required parameter")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForUniqueInArray(x reflect.Value) bool {
|
||||||
|
if x == reflect.Zero(reflect.TypeOf(x)) || x.Len() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
arrOfInterface := make([]interface{}, x.Len())
|
||||||
|
|
||||||
|
for i := 0; i < x.Len(); i++ {
|
||||||
|
arrOfInterface[i] = x.Index(i).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make(map[interface{}]bool)
|
||||||
|
for _, val := range arrOfInterface {
|
||||||
|
if m[val] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m[val] = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForUniqueInMap(x reflect.Value) bool {
|
||||||
|
if x == reflect.Zero(reflect.TypeOf(x)) || x.Len() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mapOfInterface := make(map[interface{}]interface{}, x.Len())
|
||||||
|
|
||||||
|
keys := x.MapKeys()
|
||||||
|
for _, k := range keys {
|
||||||
|
mapOfInterface[k.Interface()] = x.MapIndex(k).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make(map[interface{}]bool)
|
||||||
|
for _, val := range mapOfInterface {
|
||||||
|
if m[val] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m[val] = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInterfaceValue(x reflect.Value) interface{} {
|
||||||
|
if x.Kind() == reflect.Invalid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return x.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isZero(x interface{}) bool {
|
||||||
|
return x == reflect.Zero(reflect.TypeOf(x)).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createError(x reflect.Value, v Constraint, err string) error {
|
||||||
|
return fmt.Errorf("autorest/validation: validation failed: parameter=%s constraint=%s value=%#v details: %s",
|
||||||
|
v.Target, v.Name, getInterfaceValue(x), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toInt64(v interface{}) (int64, bool) {
|
||||||
|
if i64, ok := v.(int64); ok {
|
||||||
|
return i64, true
|
||||||
|
}
|
||||||
|
// older generators emit max constants as int, so if int64 fails fall back to int
|
||||||
|
if i32, ok := v.(int); ok {
|
||||||
|
return int64(i32), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrorWithValidationError appends package type and method name in
|
||||||
|
// validation error.
|
||||||
|
//
|
||||||
|
// Deprecated: Please use validation.NewError() instead.
|
||||||
|
func NewErrorWithValidationError(err error, packageType, method string) error {
|
||||||
|
return NewError(packageType, method, err.Error())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package autorest
|
||||||
|
|
||||||
|
// Copyright 2017 Microsoft Corporation
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// Version returns the semantic version (see http://semver.org).
|
||||||
|
func Version() string {
|
||||||
|
return "v10.12.0"
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue