Skip to content

Commit

Permalink
Merge commit '9766e70e637befbc25f43345aa1b0e0104bb1764' into perform-…
Browse files Browse the repository at this point in the history
…release
  • Loading branch information
Maria committed Mar 27, 2024
2 parents 90219f3 + 9766e70 commit 462eaa3
Show file tree
Hide file tree
Showing 22 changed files with 686 additions and 324 deletions.
17 changes: 2 additions & 15 deletions .github/workflows/build-docker-image.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
name: Docker Publish

on:
release:
types: [published]

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name }}

jobs:
build:
name: 'Docker Publish'
Expand All @@ -18,53 +15,44 @@ jobs:
packages: write
# This is used to complete the identity challenge with sigstore/fulcio when running outside of PRs.
id-token: write

steps:
# Checkout the release tag version
- name: Checkout repository ${{ env.RELEASE_TAG }}
uses: actions/checkout@v3
with:
ref: ${{ env.RELEASE_TAG }}

# Get git commit hash
- name: Get short hash
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

# Need to lower case the image name for the docker tags when publishing
- name: Downcase IMAGE_NAME variable
run: echo "IMAGE_NAME_LOWER=${IMAGE_NAME,,}" >> $GITHUB_ENV

# Sort out the image tags
- name: Set initial tag
run: echo "IMAGE_TAGS=${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}:${{ env.RELEASE_TAG }}" >> $GITHUB_ENV

- name: Add latest tag if we're not production release
if: contains(env.RELEASE_TAG, 'next')
run: echo "IMAGE_TAGS=${{ env.IMAGE_TAGS }},${{ env.REGISTRY }}/${{ env.IMAGE_NAME_LOWER }}:latest" >> $GITHUB_ENV

#debug
- name: Log the tags
run: echo "Calculated tags value => ${{ env.IMAGE_TAGS }}"

# Setup docker build tool
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3

# Login against a Docker registry
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# Extract metadata (tags, labels) for Docker
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

# Build and push Docker image with Buildx (don't push on PR)
- name: Build and push Docker image
id: build-and-push
Expand All @@ -79,8 +67,7 @@ jobs:
VCSREF=${{ env.sha_short }}
VCSTAG=${{ env.RELEASE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max

cache-to: type=gha,mode=max
# Sign the resulting Docker image digest.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
Expand All @@ -92,4 +79,4 @@ jobs:
run: cosign version
- name: Sign the published Docker image
# This step uses the identity token to provision an ephemeral certificate against the sigstore community Fulcio instance.
run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes {}@${{ steps.build-and-push.outputs.digest }}
run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign --yes {}@${{ steps.build-and-push.outputs.digest }}
52 changes: 37 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,41 @@ dotnet build
dotnet test
```

## Environment variables
The following environment variables will need to be specified for the application to run:

| Variable name | Type/Value | Description |
| ------------- | ------------- | ------------- |
| CPD_SEARCH_API_KEY | string | The Azure AI Search API key |
| CPD_INSTRUMENTATION_CONNECTIONSTRING | string | The Azure ApplicationInsights connection string |
| VCS-TAG | string | The application version |
| CPD_SEARCH_BATCH_SIZE | integer (e.g. 10/20 etc) | The batch size for queries into Contentful |
| CPD_SEARCH_ENDPOINT | string | The Azure AI Search endpoint |
| CPD_SEARCH_INDEX_NAME | string | The Azure AI Search index name to access/create |
| CPD_DELIVERY_KEY | string | The Contentful delivery API key |
| CPD_CONTENTFUL_ENVIRONMENT | string | The Contentful enviroment id |
| CPD_SPACE_ID | string | The Contentful space id |
| CPD_SEARCH_RECREATE_INDEX_ON_REBUILD | boolean (true/false) | Whether to delete the index and recreate before populating |
## Configuration
Three configuration values are required to be set in the environment:
* ``CPD_KEY_VAULT_NAME`` - the name of the keyvault to retrieve configuration from in a deployed environment
* ``CPD_CONFIG_SECTION_NAME`` - this is the key name of the root of the configuration section that stores most of the application config
* ``<CPD_CONFIG_SECTION_NAME>__Application__Version`` - the application version, note the prefix should be set to the above value, for example if `CPD_CONFIG_SECTION_NAME` was set to `DEV` then it would be `DEV__Application__Version`

The remaining application configuration is stored within an IConfiguration section, named using (1) above. In a deployed environment this should be stored in Azure Key Vault.

The configuration under the root key takes the following hierarchical structure:
```
ApplicationInsights
ConnectionString
Contentful
DeliveryKey
Environment
SpaceId
SearchIndexing
ApiKey
BatchSize
Endpoint
IndexName
RecreateIndex
```


### Local developer configuration
For development, this structure can be configured in the `secrets.json` file for the project.
Also make sure you set `LOCAL_ENVIRONMENT` to true, otherwise the application will try to initialise key vault. `LOCAL_ENVIRONMENT` is not required for any other scenario.

### Deployed environments
Key names for secrets are built from the full path to the config key, separating each level with two dash characters (--). So if `CPD_CONFIG_SECTION_NAME` is set to `DEV` then it would be:
```
DEV--ApplicationInsights--ConnectionString
DEV--ApplicationVersion
DEV--Contentful--DeliveryKey
...
```
Note this format is for **Key Vault only**.
Original file line number Diff line number Diff line change
@@ -1,15 +1,44 @@
using Azure;
using Azure.Core;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;
using Childrens_Social_Care_CPD_Indexer.Core;
using Microsoft.Extensions.Logging;
using NSubstitute.ExceptionExtensions;
using System.Diagnostics.CodeAnalysis;

namespace Childrens_Social_Care_CPD_Indexer.Tests.Core;

public class ResourcesIndexerTest
internal sealed class MockResponse : Response
{
private ILogger _logger;
public override int Status => 404;
public override string ReasonPhrase => string.Empty;

public override Stream? ContentStream
{
get => new MemoryStream();
set => throw new NotImplementedException();
}
public override string ClientRequestId
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}

public override void Dispose() => throw new NotImplementedException();
protected override bool ContainsHeader(string name) => false;
protected override IEnumerable<HttpHeader> EnumerateHeaders() => Array.Empty<HttpHeader>();
protected override bool TryGetHeader(string name, [NotNullWhen(true)] out string? value) {
value = null;
return false;
}
protected override bool TryGetHeaderValues(string name, [NotNullWhen(true)] out IEnumerable<string>? values) => throw new NotImplementedException();
}

public class ResourcesIndexerTests
{
private ILogger<ResourcesIndexer> _logger;
private SearchIndexClient _client;
private IDocumentFetcher _documentFetcher;
private ResourcesIndexer _sut;
Expand All @@ -21,16 +50,15 @@ public void Setup()
_client = Substitute.For<SearchIndexClient>();
_documentFetcher = Substitute.For<IDocumentFetcher>();

_sut = new ResourcesIndexer(_client, _documentFetcher, _logger);
_sut = new ResourcesIndexer(_client, _documentFetcher, _logger, MockTelemetryClient.Create());
}

[Test]
public async Task DeleteIndexAsync_Skips_Deletion_If_Index_Does_Not_Exist()
{
// arrange
var response = Substitute.For<Response<SearchIndex>>();
response.HasValue.Returns(false);
_client.GetIndexAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(response));
var exception = new RequestFailedException(new MockResponse());
_client.GetIndexAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Throws(exception);

// act
await _sut.DeleteIndexAsync("foo");
Expand Down Expand Up @@ -77,9 +105,8 @@ public async Task DeleteIndexAsync_Logs_Failure_To_Delete_Index()
public async Task CreateIndexAsync_Creates_The_Index()
{
// arrange
var getIndexResult = Substitute.For<Response<SearchIndex>>();
getIndexResult.HasValue.Returns(false);
_client.GetIndexAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Returns(Task.FromResult(getIndexResult));
var exception = new RequestFailedException(new MockResponse());
_client.GetIndexAsync(Arg.Any<string>(), Arg.Any<CancellationToken>()).Throws(exception);

SearchIndex? searchIndex = null;
await _client.CreateIndexAsync(Arg.Do<SearchIndex>(x => searchIndex = x), Arg.Any<CancellationToken>());
Expand Down Expand Up @@ -141,7 +168,6 @@ public async Task PopulateIndexAsync_Uploads_Documents_In_Multiple_Batches()

await _sut.PopulateIndexAsync("foo", 10);

await client.Received(2)
.UploadDocumentsAsync(documents, Arg.Any<IndexDocumentsOptions>(), Arg.Any<CancellationToken>());
await client.Received(2).UploadDocumentsAsync(documents, Arg.Any<IndexDocumentsOptions>(), Arg.Any<CancellationToken>());
}
}

This file was deleted.

32 changes: 32 additions & 0 deletions src/Childrens-Social-Care-CPD-Indexer.Tests/MockTelemetryClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using System.Collections.Concurrent;

namespace Childrens_Social_Care_CPD_Indexer.Tests;

internal sealed class MockTelemetryChannel : ITelemetryChannel
{
public ConcurrentBag<ITelemetry> SentTelemtries = [];
public bool IsFlushed { get; private set; }
public bool? DeveloperMode { get; set; }
public string EndpointAddress { get; set; } = string.Empty;

public void Send(ITelemetry item) => SentTelemtries.Add(item);
public void Flush() => IsFlushed = true;
public void Dispose() {}
}

internal static class MockTelemetryClient
{
public static TelemetryClient Create()
{
var mockTelemetryConfig = new TelemetryConfiguration
{
TelemetryChannel = new MockTelemetryChannel(),
};

return new TelemetryClient(mockTelemetryConfig);
}

}
Loading

0 comments on commit 462eaa3

Please sign in to comment.