Skip to content

Commit 42ca4b5

Browse files
committed
fix: quality of life improvements and further docs
- replaces all calls of GetUserFiles() with GetUserFilesWithPrefix() since it should be much more performant and less prone for errors - adding shorthand aliases for `--config` and `--data-directory` to make usage easier - formatting - adding documentation to try and clarify different uses between running as a cli and running as a job
1 parent 2872f35 commit 42ca4b5

9 files changed

Lines changed: 51 additions & 24 deletions

File tree

README.md

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# sda-bpctl
22

3-
43
A tool that can be used to deal with administrative workflows for the big picture project. It supports three primary functions, making data ingestion, assigning accession ids to each ingested file, and creating a dataset for all files ingested with a accession id.
54

6-
It can be used either locally as a cli tool or be packaged and run as a job in kubernetes.
5+
It can be used either locally as a cli tool or be packaged and run as a job in kubernetes. This is on one hand powerfull and on the other hand, sometimes confusing and unintuitive, in an attempt to clarify the differance in logic based on how the tool is used the terms used will be **job** and **cli** in **bold** when describing the different logics.
6+
7+
The core logic of this tool is wrapping logic around the sensetive data archive (SDA) api. To fully understand how this is expected to work you should be familiar with the sda and it's api.
78

89
### installation
910

@@ -27,6 +28,7 @@ Commands must be one of:
2728
- `dataset`
2829
- `mail`
2930
- `job`
31+
- `render`
3032

3133
#### examples
3234

@@ -63,10 +65,8 @@ and apply it using `kubectl`:
6365
kubectl apply -f job.yaml
6466
```
6567

66-
6768
will render a job.yaml manifest for you based on the configuration values you have supplied
6869

69-
7070
### configuration
7171

7272
bpctl can consume configuration from either `config.yaml` or from environment variables. If both are supplied then the environment variables will take priority. If using config.yaml it is expected to be located in the root directory of the project. It can also be supplied by using the `--config` flag if located elsewhere.
@@ -77,6 +77,8 @@ see the `config.yaml.example` for a base template with what fields to fill
7777
| USER_ID | "user-1234" | The user ID for the uploader, acts as identifier for the uploaded data | `ingest`, `accession`, `dataset`, `job`, `render` |
7878
| DATASET_ID | "aa-Dataset-abc" | The ID that will be set for the finalized dataset, will be used during the `dataset` command | `ingest`, `accession`, `dataset`, `job`, `mail`, `render` |
7979
| DATASET_FOLDER | "DATASET_ABC" | The folder where the uploaded data resides in s3inbox | `ingest`, `accession`, `dataset`, `job`, `mail`, `render` |
80+
| JOB_TIMEOUT | 3 | A integer value, representing the number of minutes before the job times out when waiting for `accession` | `job`, `render` |
81+
| JOB_POLL_RATE | 2 | A integer value, representing the number of miutes between each polling intervall when waiting for `accession`, needs to be less than the `JOB_TIMEOUT` value | `job`, `render` |
8082
| JOB_EXPECTED_NR_FILES | 0 | The expected number of files to be part of the finalized dataset, set this when using `render` to include it in the rendered job.yaml | `job`, `render` |
8183
| CLIENT_API_HOST | "https://api.example.com" | The hostname for the SDA API to communicate with | `ingest`, `accession`, `dataset`, `job` |
8284
| CLIENT_ACCESS_TOKEN | "youraccesstoken" | The access token to authenticate towards the client api host | Yes | `ingest`, `accession`, `dataset`, `job` |
@@ -89,6 +91,32 @@ see the `config.yaml.example` for a base template with what fields to fill
8991
| DB_SECRET_NAME | "db-secret" | The name of the kubernetes secret that holds connection details for the sda database | `job` ,`render` |
9092
| CERT_SECRET_NAME | "cert-secret" | The name of the kubernetes secret that holds a tls certificate to use | `job`, `render` |
9193

94+
### ingest
95+
96+
The ingtest command will lookup all the files for the `USER_ID` that resides in `DATASET_FOLDER`, filter out all files that are not in either a directory `LANDING_PAGE` or `PRIVATE` and any file that does not have the event `uploaded`.
97+
98+
**job:** the number of files retrieved will be compared to the `JOB_EXPECTED_NR_FILES` value, if they match the files will be sent for ingestion. If not the job will fail.
99+
100+
**cli:** the files found will be sent for ingestion without evaluation. In that case the responsibility is on the user to ensure with a `--dry-run` before that the number of files are the desired ammount.
101+
102+
Files sent to ingestion are done so trough the sda api `POST /ingest` endpoint.
103+
104+
### accession
105+
106+
The accession command will get a list of files for the `USER_ID` that resides in `DATASET_FOLDER` and have the event `verified`.
107+
108+
**job:** will poll the api according to `JOB_POLL_RATE` and wait untill it finds the ammount of files that matches `JOB_EXPECTED_NR_FILES` or untill it times out according to `JOB_TIMEOUT`. When the expected number of files are found it will send a request to the sda api `POST /accession` endpoint with the files.
109+
110+
**cli:** will try create a file called `<DATASET_FODLER>-fileIDs.txt` in the `--data-directory` directory. It will retreive the list of files and after successfull call to `POST /accession` it will write the accessionIDs to the file `<DATASET_FOLDER>-fileIDs.txt`. This is legacy logic owned from the `ingestor.sh` scipt and makes it so that you can store a intermidate state and keep track of the accession ids retrieved between runs of `accession` and `dataset`.
111+
112+
### dataset
113+
114+
The dataset command will retrieve a list of accessionIDs and send a request to the sda api `POST /dataset`
115+
116+
**job:** will consume the list of accessionIDs by a in memory variable produced by the previous step in `accession` and send a list of files to be mapped to a dataset to `POST /dataset/create`
117+
118+
**cli:** will try to read from `<DATASET_FOLDER>-fileIDs.txt` to identify the files to be included in a dataset. If the file cannot be found it will make a call to `GET /user/files?path_prefix=<DATASET_FOLDER>` to find them and send a request to `POST /dataset/create` with the files.
119+
92120
### testing
93121

94122
Unit tests using [pkg.go.dev/testing](https://pkg.go.dev/testing)

internal/accession/accession.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ var accessionCmd = &cobra.Command{
5151
}
5252
defer file.Close() //nolint:errcheck
5353

54-
files, err := api.GetUsersFiles()
54+
files, err := api.GetUsersFilesWithPrefix()
5555
if err != nil {
5656
return err
5757
}
@@ -80,13 +80,13 @@ var accessionCmd = &cobra.Command{
8080
func init() {
8181
cmd.AddCommand(accessionCmd)
8282
accessionCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Toggles dry-run mode. Dry run will not run any state changing API calls")
83-
accessionCmd.Flags().StringVar(&configPath, "config", "config.yaml", "Path to configuration file")
84-
accessionCmd.Flags().StringVar(&dataDirectory, "data-directory", "data", "Path to directory to write / read intermediate files for stableIDs and fileIDs")
83+
accessionCmd.Flags().StringVarP(&configPath, "config", "c", "config.yaml", "Path to configuration file")
84+
accessionCmd.Flags().StringVarP(&dataDirectory, "data-directory", "d", "data", "Path to directory to write / read intermediate files for stableIDs and fileIDs")
8585
}
8686

8787
func Run(api client.APIClient, datasetFolder string, userID string) ([]string, error) {
8888
slog.Info("starting accession")
89-
files, err := api.GetUsersFiles()
89+
files, err := api.GetUsersFilesWithPrefix()
9090
if err != nil {
9191
return nil, err
9292
}
@@ -101,7 +101,6 @@ func Run(api client.APIClient, datasetFolder string, userID string) ([]string, e
101101
return accessionIDs, nil
102102
}
103103

104-
105104
func postAccessionIDs(api client.APIClient, paths []string, userID string) ([]string, error) {
106105
var accessionIDs []string
107106
for _, filepath := range paths {

internal/client/client.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ type Client struct {
2828
httpClient *http.Client
2929
}
3030

31-
var retryableStatusCodes = map[int]bool {
32-
http.StatusBadGateway: true,
33-
http.StatusGatewayTimeout: true,
31+
var retryableStatusCodes = map[int]bool{
32+
http.StatusBadGateway: true,
33+
http.StatusGatewayTimeout: true,
3434
http.StatusInternalServerError: true,
35-
http.StatusServiceUnavailable: true,
35+
http.StatusServiceUnavailable: true,
3636
// as of (2026-01-21) we believe there can be erronus responses from the API of 400 bad request that we wish to retry on
3737
http.StatusBadRequest: true,
3838
}
@@ -199,7 +199,7 @@ func (c *Client) WaitForAccession(target int, interval time.Duration, timeout ti
199199
}
200200

201201
func (c *Client) getVerifiedFilePaths() ([]string, error) {
202-
files, err := c.GetUsersFiles()
202+
files, err := c.GetUsersFilesWithPrefix()
203203
if err != nil {
204204
return nil, err
205205
}

internal/dataset/dataset.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ var datasetCmd = &cobra.Command{
8080
func init() {
8181
cmd.AddCommand(datasetCmd)
8282
datasetCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Toggles dry-run mode. Dry run will not run any state changing API calls")
83-
datasetCmd.Flags().StringVar(&configPath, "config", "config.yaml", "Path to configuration file")
84-
datasetCmd.Flags().StringVar(&dataDirectory, "data-directory", "data", "Path to directory to write / read intermediate files for stableIDs and fileIDs")
83+
datasetCmd.Flags().StringVarP(&configPath, "config", "c", "config.yaml", "Path to configuration file")
84+
datasetCmd.Flags().StringVarP(&dataDirectory, "data-directory", "d", "data", "Path to directory to write / read intermediate files for stableIDs and fileIDs")
8585
}
8686

8787
type Payload struct {
@@ -107,7 +107,7 @@ func getFileIDs(datasetFolder string, api client.APIClient) ([]string, error) {
107107
var fileIDsList []string
108108
filePath := helpers.GetFileIDsPath(dataDirectory, datasetFolder)
109109
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
110-
files, err := api.GetUsersFiles()
110+
files, err := api.GetUsersFilesWithPrefix()
111111
if err != nil {
112112
return nil, err
113113
}

internal/dataset/dataset_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import (
1414
)
1515

1616
type mockClient struct {
17-
UserFiles []models.FileInfo
17+
UserFiles []models.FileInfo
1818
UserFilesWithPrefix []models.FileInfo
19-
Response *http.Response
19+
Response *http.Response
2020
}
2121

2222
func (m *mockClient) GetUsersFiles() ([]models.FileInfo, error) {

internal/ingest/ingest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ var ingestCmd = &cobra.Command{
4848
func init() {
4949
cmd.AddCommand(ingestCmd)
5050
ingestCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Toggles dry-run mode. Dry run will not run any state changing API calls")
51-
ingestCmd.Flags().StringVar(&configPath, "config", "config.yaml", "Path to configuration file")
51+
ingestCmd.Flags().StringVarP(&configPath, "config", "c", "config.yaml", "Path to configuration file")
5252
}
5353

5454
func Run(api client.APIClient, datasetFolder string, userID string, expectedFiles int) (int, error) {

internal/job/job.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ var jobCmd = &cobra.Command{
5151

5252
func init() {
5353
cmd.AddCommand(jobCmd)
54-
jobCmd.Flags().StringVar(&configPath, "config", "config.yaml", "Path to configuration file")
54+
jobCmd.Flags().StringVarP(&configPath, "config", "c", "config.yaml", "Path to configuration file")
5555
}
5656

5757
func runJob(expectedFiles int) error {

internal/mail/mail.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ var mailCmd = &cobra.Command{
4343
func init() {
4444
cmd.AddCommand(mailCmd)
4545
mailCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Toggles dry-run mode. Dry run will send all emails to the address in configuration.Email (env or yaml conf)")
46-
mailCmd.Flags().StringVar(&configPath, "config", "config.yaml", "Path to configuration file")
47-
mailCmd.Flags().StringVar(&dataDirectory, "data-directory", "data", "Directory to retrieve files from to attach in mail notifications")
46+
mailCmd.Flags().StringVarP(&configPath, "config", "c", "config.yaml", "Path to configuration file")
47+
mailCmd.Flags().StringVarP(&dataDirectory, "data-directory", "d", "data", "Directory to retrieve files from to attach in mail notifications")
4848
}
4949

5050
type Mail struct {

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313
_ "github.com/NBISweden/sda-bpctl/internal/mail"
1414
)
1515

16-
var version = "v1.1.2"
16+
var version = "v1.1.4"
1717

1818
func main() {
1919
slog.Info("running", "version", version)

0 commit comments

Comments
 (0)