sources/Google.Solutions.Apis/Compute/ResourceMetadataExtensions.cs (233 lines of code) (raw):
//
// Copyright 2019 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
//
using Google.Apis.Compute.v1;
using Google.Apis.Compute.v1.Data;
using Google.Solutions.Apis.Client;
using Google.Solutions.Apis.Locator;
using Google.Solutions.Common;
using Google.Solutions.Common.Diagnostics;
using Google.Solutions.Common.Util;
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace Google.Solutions.Apis.Compute
{
/// <summary>
/// Extension methods for mutating project and instance metadata.
/// </summary>
public static class ResourceMetadataExtensions
{
private const uint DefaultAttempts = 6;
/// <summary>
/// Modify metadata using optimistic concurrency control.
/// </summary>
private static async Task UpdateMetadataAsync(
Func<Task<Metadata>> readMetadata,
Func<Metadata, Task> writeMetadata,
Action<Metadata> updateMetadata,
uint maxAttempts)
{
for (var attempt = 1; ; attempt++)
{
//
// NB. Metadata must be updated all-at-once. Therefore,
// fetch the existing entries first before merging them
// with the new entries.
//
var metadata = await readMetadata()
.ConfigureAwait(false);
updateMetadata(metadata);
try
{
await writeMetadata(metadata)
.ConfigureAwait(false);
break;
}
catch (Exception e) when (e.Unwrap() is GoogleApiException apiException)
{
if (attempt == maxAttempts)
{
//
// That's enough, give up.
//
CommonTraceSource.Log.TraceWarning(
"SetMetadata failed with {0} (code error {1})", e.Message,
apiException.Error?.Code);
throw;
}
else if (
apiException.HttpStatusCode == HttpStatusCode.ServiceUnavailable ||
apiException.Error != null && apiException.Error.Code == 412)
{
//
// 412 indicates a conflict, meaning we lost the
// optimisitic concurrency control race agains
// someone else.
//
// 503 indicates that the API is being flaky.
//
// In both cases, back off and retry the
// read/update/write operation.
//
var backoff = TimeSpan.FromMilliseconds(10 * attempt);
CommonTraceSource.Log.TraceWarning(
"SetMetadata failed with {0} (code error {1}) - retrying after {2}",
e.Message,
apiException.Error?.Code,
backoff);
await Task
.Delay(backoff)
.ConfigureAwait(false);
}
else
{
CommonTraceSource.Log.TraceWarning(
"Setting metadata failed {0} (code error {1})", e.Message,
apiException.Error?.Code);
throw;
}
}
}
}
//---------------------------------------------------------------------
// Extension methods for modifying project metadata.
//---------------------------------------------------------------------
/// <summary>
/// Project/common instance metadata.
/// </summary>
public static async Task UpdateMetadataAsync(
this ProjectsResource resource,
string projectId,
Action<Metadata> updateMetadata,
CancellationToken token,
uint maxAttempts = DefaultAttempts)
{
using (CommonTraceSource.Log.TraceMethod().WithParameters(projectId))
{
await UpdateMetadataAsync(
async () =>
{
var project = await resource.Get(projectId)
.ExecuteAsync(token)
.ConfigureAwait(false);
return project.CommonInstanceMetadata;
},
async metadata =>
{
await resource.SetCommonInstanceMetadata(
metadata,
projectId)
.ExecuteAndAwaitOperationAsync(
projectId,
token)
.ConfigureAwait(false);
},
updateMetadata,
maxAttempts)
.ConfigureAwait(false);
}
}
/// <summary>
/// Adds or overwrite a metadata key/value pair.
/// Existing metadata is kept as is.
/// </summary>
public static Task AddMetadataAsync(
this ProjectsResource resource,
string projectId,
string key,
string value,
CancellationToken token)
{
return AddMetadataAsync(
resource,
projectId,
new Metadata()
{
Items = new List<Metadata.ItemsData>()
{
new Metadata.ItemsData()
{
Key = key,
Value = value
}
}
},
token);
}
public static Task AddMetadataAsync(
this ProjectsResource resource,
string projectId,
Metadata metadata,
CancellationToken token)
{
return UpdateMetadataAsync(
resource,
projectId,
existingMetadata =>
{
existingMetadata.Add(metadata);
},
token);
}
//---------------------------------------------------------------------
// Extension methods for modifying instance metadata.
//---------------------------------------------------------------------
/// <summary>
/// Modify instance metadata.
/// </summary>
public static async Task UpdateMetadataAsync(
this InstancesResource resource,
InstanceLocator instanceRef,
Action<Metadata> updateMetadata,
CancellationToken token,
uint maxAttempts = DefaultAttempts)
{
using (CommonTraceSource.Log.TraceMethod().WithParameters(instanceRef))
{
await UpdateMetadataAsync(
async () =>
{
var instance = await resource.Get(
instanceRef.ProjectId,
instanceRef.Zone,
instanceRef.Name)
.ExecuteAsync(token)
.ConfigureAwait(false);
return instance.Metadata;
},
async metadata =>
{
await resource.SetMetadata(
metadata,
instanceRef.ProjectId,
instanceRef.Zone,
instanceRef.Name)
.ExecuteAndAwaitOperationAsync(
instanceRef.ProjectId,
token)
.ConfigureAwait(false);
},
updateMetadata,
maxAttempts)
.ConfigureAwait(false);
}
}
/// <summary>
/// Adds or overwrite a metadata key/value pair.
/// Existing metadata is kept as is.
/// </summary>
public static Task AddMetadataAsync(
this InstancesResource resource,
InstanceLocator instanceRef,
string key,
string value,
CancellationToken token)
{
return AddMetadataAsync(
resource,
instanceRef,
new Metadata()
{
Items = new List<Metadata.ItemsData>()
{
new Metadata.ItemsData()
{
Key = key,
Value = value
}
}
},
token);
}
public static Task AddMetadataAsync(
this InstancesResource resource,
InstanceLocator instanceRef,
Metadata metadata,
CancellationToken token)
{
return UpdateMetadataAsync(
resource,
instanceRef,
existingMetadata =>
{
existingMetadata.Add(metadata);
},
token);
}
//---------------------------------------------------------------------
// Extension methods for reading metadata.
//---------------------------------------------------------------------
public static bool? GetFlag(this Instance instance, Project project, string flag)
{
//
// NB. The instance value always takes precedence,
// even if it's false.
//
var instanceValue = instance.Metadata.GetFlag(flag);
if (instanceValue != null)
{
return instanceValue.Value;
}
var projectValue = project.CommonInstanceMetadata.GetFlag(flag);
if (projectValue != null)
{
return projectValue.Value;
}
return null;
}
public static bool? GetFlag(this Project project, string flag)
{
return project.CommonInstanceMetadata.GetFlag(flag);
}
}
}