Skip to content

Conversation

@andre-urbani
Copy link
Contributor

@andre-urbani andre-urbani commented Nov 27, 2025

What

Add functionality to purge cloudflare cache on publication of files (via PUT /state endpoint)
Jira - https://officefornationalstatistics.atlassian.net/browse/DIS-4109

How to review

Run the dataset-catalogue stack and make sure you have the latest version which includes the cloud-flare-stub

Make sure you have these env vars set locally -

ENABLE_CLOUDFLARE_SDK=false
CLOUDFLARE_ZONE_ID="9f1eec58caedd8e902395a065c120073"
CLOUDFLARE_API_TOKEN="test-token"
CLOUDFLARE_API_URL="http://cloud-flare-stub:22500"

Create a new dataset and version, and make sure when creating the version that there is a file in the local db that matches the file in the distributions array
Use the PUT /state endpoint for the version to transition through the states until the published state
After transitioning to the published state, check the docker logs for dp-dataset-api and cloud-flare-stub and confirm that the cache purge has run successfully. The dataset logs should look something like -

{
  "event": "purging cloudflare cache",
  "data": {
    "dataset_id": "static-test-dataset",
    "edition": "time-series",
    "prefixes": [
      "www.ons.gov.uk/datasets/static-test-dataset",
      "www.ons.gov.uk/datasets/static-test-dataset/editions",
      "www.ons.gov.uk/datasets/static-test-dataset/editions/time-series/versions",
      "api.beta.ons.gov.uk/v1/datasets/static-test-dataset",
      "api.beta.ons.gov.uk/v1/datasets/static-test-dataset/editions",
      "api.beta.ons.gov.uk/v1/datasets/static-test-dataset/editions/time-series/versions"
    ]
  }
}
{
  "event": "successfully triggered cloudflare cache purge",
  "data": {
    "dataset_id": "static-test-dataset",
    "edition": "time-series",
    "version": "1"
  }
}

You can also test that the real Cloudlfare API is being hit by setting ENABLE_CLOUDFLARE_SDK=true, following the same steps as above, and then you should get an error in the logs. Something like this -

{
  "event": "failed to purge cloudflare cache",
  "errors": [
    {
      "message": "failed to purge cache: Authentication error (10000)"
    }
  ]
}

Which is just a result of the api token being incorrect (this will be tested in sandbox with a real api token)

Confirm changes make sense, tests pass

Who can review

Anyone

@andre-urbani andre-urbani requested a review from a team as a code owner November 27, 2025 11:11
Copy link
Contributor

@aryan-p03 aryan-p03 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments about the client setup, logging and keeping dataset-api logic separate from the cloudflare package.
Nice to see testcontainers being used for unit tests

return err
}

func (svc *Service) initCloudflareClient(ctx context.Context) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some of the checks in this function are repeated in initialise.go

Comment on lines +237 to +239
if err := svc.initCloudflareClient(ctx); err != nil {
log.Error(ctx, "failed to initialise cloudflare client, continuing without cache purging", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return an error, if the client fails to setup then the service shouldn't start up

Comment on lines +184 to +214
func (e *Init) DoGetCloudflareClient(ctx context.Context, cfg *config.Configuration) (cloudflare.Clienter, error) {
if cfg.CloudflareAPIToken == "" || cfg.CloudflareZoneID == "" {
log.Info(ctx, "cloudflare integration disabled: missing API token or zone ID")
return nil, nil
}

var client cloudflare.Clienter
var err error

// for local mock server when SDK is disabled
if !cfg.EnableCloudflareSDK && cfg.CloudflareAPIURL != "" {
client, err = cloudflare.New(
cfg.CloudflareAPIToken,
cfg.CloudflareZoneID,
cfg.EnableCloudflareSDK,
cfg.CloudflareAPIURL,
)
} else {
client, err = cloudflare.New(
cfg.CloudflareAPIToken,
cfg.CloudflareZoneID,
cfg.EnableCloudflareSDK,
)
}

if err != nil {
return nil, err
}

return client, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the setup within the cloudflare package will bring up the errors with missing configs so we don't need to check that here. We just need to check if cloudflare is enabled here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the comment about the mocks on component test setup, we would be able to have a simple step saying something like:
the following URL's should have been purged: followed by a list of the URL's we expect to be purged

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the component test setup, can the cloudflare package have mocks generated that can be used here instead of making the mocks here?

Comment on lines +131 to +139
func buildPrefixes(datasetID, editionID string) []string {
return []string{
fmt.Sprintf("www.ons.gov.uk/datasets/%s", datasetID),
fmt.Sprintf("www.ons.gov.uk/datasets/%s/editions", datasetID),
fmt.Sprintf("www.ons.gov.uk/datasets/%s/editions/%s/versions", datasetID, editionID),
fmt.Sprintf("api.beta.ons.gov.uk/v1/datasets/%s", datasetID),
fmt.Sprintf("api.beta.ons.gov.uk/v1/datasets/%s/editions", datasetID),
fmt.Sprintf("api.beta.ons.gov.uk/v1/datasets/%s/editions/%s/versions", datasetID, editionID),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the previous comment, this function should be in the dataset-api and not the SDK package. I would also avoid hard-coded domain names and instead build off config values

Comment on lines +62 to +63
// PurgeCacheByPrefix purges the Cloudflare cache for dataset-related URLs
func (c *Client) PurgeCacheByPrefix(ctx context.Context, datasetID, editionID string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function shouldn't be in the SDK as it is specific to the dataset-api. The SDK should just accept a list of prefixes which it then purges (so any service can use the function).
The dataset-api should be responsible for building the prefixes and then this list will be passed to the SDK

Comment on lines +847 to +853
if api.cloudflareClient != nil {
if err := api.cloudflareClient.PurgeCacheByPrefix(ctx, datasetID, edition); err != nil {
log.Error(ctx, "failed to purge cloudflare cache", err, logData)
} else {
log.Info(ctx, "successfully triggered cloudflare cache purge", logData)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the client is nil here then there is no trace and cache will not be cleared which is a big problem for support. Ideally the client should be confirmed as not nil at setup so we don't have the nil check here.
If possible, it would be nice to have the list of URL's purged in the log.Info so we check them in environment testing.
I see that we only have the error log when purge fails as we don't want to block publishing. Might be worth asking Fran if this is ok since the purge can fail but the dataset will be published

Comment on lines +833 to +840
_, err = api.smDatasetAPI.AmendVersion(r.Context(), vars, versionUpdate)
if err != nil {
handleVersionAPIErr(ctx, err, w, logData)
return
}

if stateUpdate.State == models.PublishedState && updatedVersion.Distributions != nil && len(*updatedVersion.Distributions) > 0 {
err = api.publishDistributionFiles(ctx, updatedVersion, logData)
if stateUpdate.State == models.PublishedState && currentVersion.Distributions != nil && len(*currentVersion.Distributions) > 0 {
err = api.publishDistributionFiles(ctx, currentVersion, logData)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming the change to _ instead of updatedVersion is due to the linter which looks ok.
However currentVersion is being used on lines 839 and 840 where it was previously updatedVersion. I think it should remain as updatedVersion unless there's an issue I'm not seeing here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants