-
Notifications
You must be signed in to change notification settings - Fork 124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for docker-credential-stores through environment #1022
base: main
Are you sure you want to change the base?
Add support for docker-credential-stores through environment #1022
Conversation
Allow for a docker-credential-helper to be set as DOCKER_AUTH_CONFIG environment variable. Takes precedence over the config.json file.
Hi Niklas 👋 Thanks for your contribution and for taking the time to provide such a details explanation! I appreciate the effort, but I feel the approach might be a bit more complex than necessary to achieve your goal. Please take a look at the following alternative: diff --git a/Sources/tart/Credentials/DockerConfigCredentialsProvider.swift b/Sources/tart/Credentials/DockerConfigCredentialsProvider.swift
index 6be901f..a50de8e 100644
--- a/Sources/tart/Credentials/DockerConfigCredentialsProvider.swift
+++ b/Sources/tart/Credentials/DockerConfigCredentialsProvider.swift
@@ -2,7 +2,11 @@ import Foundation
class DockerConfigCredentialsProvider: CredentialsProvider {
func retrieve(host: String) throws -> (String, String)? {
- let dockerConfigURL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".docker").appendingPathComponent("config.json")
+ let dockerConfigURL = if let tartDockerConfig = ProcessInfo.processInfo.environment["TART_DOCKER_CONFIG"] {
+ URL(fileURLWithPath: tartDockerConfig)
+ } else {
+ FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".docker").appendingPathComponent("config.json")
+ }
if !FileManager.default.fileExists(atPath: dockerConfigURL.path) {
return nil
} The test(s) can be added to |
I'm all in for a simpler approach. Also, falling back to a standard
I think we're better off with integration tests since there's no need to introduce unnecessary abstractions to the Note that you can also have integration tests in Swift (see /cc @fkorotkov |
Checking the integration tests I am not sure I understand the approach entirely. How would you
|
For that you may need to extend the tart/integration-tests/tart.py Lines 24 to 32 in f682970
This can be achieved by simply making sure that We currently use tart/integration-tests/docker_registry.py Lines 10 to 12 in 0b693f6
|
+1 for @edigaryev's:
IMO it's better to fail fast then fallback. I do like @nholloh's |
…dence based on PR comments
@edigaryev thanks for your explanation and input. I'm working on the changes and there may be multiple pushes in the process to get the integration tests right. I'll post an update here once I am done. |
The Another point is that |
Since this change is a proposal in the context of CI, specifically due to the issues we're facing on GitLab, I see value in following GitLab's naming scheme. Additionally, the
While I see your point that the configuration may contain more information than just authentication, the new environment variable I am proposing is carrying the file's contents rather than a path to the file. Using the same naming scheme for entirely different content could lead to confusion around its behaviour. Providing a file path instead of the content would not resolve the issue mentioned above without further workarounds. Therefore, I still think explicitly choosing a different name makes sense. Alternatively, how about |
This seems like a rather stretched assumption because Tart is used in various environments, not just GitLab, and there our users may find this environment variable useful as well.
Just to clarify, is there a reason this approach doesn't this work for you? script:
- echo "$DOCKER_CONFIG_BASE64" | base64 -d > ./config.json
- export TART_DOCKER_CONFIG=./config.json
- tart ... It's used in GitLab for The file will be created in the project's directory and shouldn't cause any conflicts with other jobs. |
Coming from iOS development, I don't really know my way around the different tools used in infra and hosting. That is, if it really is GitLab who deviate from the convention to pass file paths into a tool as environment variable for the OCI registry authentication, then I am completely with you. In this case we should absolutely pass the file path as env var. To make things align with GitLab's way of providing credentials, maybe we could add the feature to the prepare command of the gitlab-tart-executor. It could then take a |
@nholloh that sounds like good trade-off to me, especially since we'd still need to change the GitLab Tart Executor to support this because GitLab Runner prepends See for example how we do this for However, I still think that a more appropriate name for the environment variable should be a suffix to |
@edigaryev sounds good to me. I'm currently working on the changes and noticed one more detail: In my opinion the behaviour for File not found should differ between the default docker location and a file I explicitly request to be used through the Whats your opinion on this? |
Sounds reasonable to me, as long as the user will get a clear error message explaining the what's wrong. |
Done, the changes are implemented. Feel free to take a look. The error message now reads:
It uses the standard apple keys and error codes for the FileNotFound error. Also, I updated the initial description of the pull request to match the current state of implementation after our discussion. |
Just a remark! As example the authent to AWS ECR, using AWS secret/access key or ephemeral token. Azure too. |
Would this not be covered through the existing implementation? private func retrieveCredentials(for host: String, from config: DockerConfig) throws -> (String, String)? {
if let credentials = config.auths?[host]?.decodeCredentials() {
return credentials
}
if let helperProgram = try config.findCredHelper(host: host) {
return try executeHelper(binaryName: "docker-credential-\(helperProgram)", host: host)
}
return nil
} |
At minima with your implementation, on aws or azure you must create a temporary token to authent on private registry in your script such aws aws ecr-login.... But it not enough if you want integrate docker credentials helper mechanism with existing plugin. Take a look at: https://github.com/docker/docker-credential-helpers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, try make the smallest amount of changes necessary to get your functionality across.
This will save both your (writing the code) and our time (reviewing it).
@@ -6,6 +6,16 @@ extension Collection { | |||
} | |||
} | |||
|
|||
extension NSError { | |||
static func fileNotFoundError(url: URL, message: String = "") -> NSError { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just throw a CredentialsProviderError.Failed(...)
instead of declaring a new error here.
return try retrieveCredentials(for: host, from: config) | ||
} | ||
|
||
// MARK: - Private |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nicer if we could avoid these marks, we don't use them anywhere else in the codebase.
@@ -1,16 +1,54 @@ | |||
import Foundation | |||
|
|||
class DockerConfigCredentialsProvider: CredentialsProvider { | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like an extraneous space compared to the rest of the codebase.
def test_authenticated_push_invalid_env_path_error(self, tart, vm_with_random_disk, docker_registry_authenticated): | ||
env = { "TART_DOCKER_CONFIG": "/temp/this-file-does-not-exist" } | ||
|
||
_, stderr, returncode = tart.run( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can just declare a new method: run_with_returncode()
that defaults to raise_on_nonzero_returncode=False
to avoid the code churn for the rest of the run()
calls.
The run()
itself will call the new run_with_returncode()
under the hood, passing all of its arguments to it and setting raise_on_nonzero_returncode=True
.
@pytest.mark.dependency() | ||
@pytest.mark.parametrize("docker_registry_authenticated", [("user1", "pass1")], indirect=True) | ||
def test_authenticated_push_from_docker_config(self, tart, vm_with_random_disk, docker_registry_authenticated): | ||
with tempfile.NamedTemporaryFile(delete=False) as tf: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use a temporary file when you can write to the destination directly?
with open(os.path.expanduser("~/.docker/config.json"), "w") as f:
...
credentials = request.param if hasattr(request, "param") else ("testuser", "testpassword") | ||
|
||
with DockerRegistry(credentials=credentials) as docker_registry: | ||
yield docker_registry |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing newline at the end of the file.
@@ -1,16 +1,54 @@ | |||
import Foundation | |||
|
|||
class DockerConfigCredentialsProvider: CredentialsProvider { | |||
|
|||
func retrieve(host: String) throws -> (String, String)? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like this is too much indirection for such a simple function. We now have four new functions containing only 5 lines of code on average, and they're only being called from a single place, thus offering no code re-use that functions/methods normally provide.
I think we could get away with just a single new function:
private func dockerConfig() throws -> Data? { ... }
That will either read from TART_DOCKER_CONFIG
, throw an exception, read from the default location or return nil
.
You can then call it from the retrieve()
accordingly by introducing a minimal amount of changes.
Motivation
Larger organizations running tart often also run their own OCI registry, where VM images are being managed. Different departments within the organization may have limited access to each others images. That is, they may require different sets of credentials to authenticate with a given OCI registry.
Currently, tart only supports a docker-credential-store through a file in
~/.docker/config.json
within the host system. This is not always feasible, as 2 concurrently issued builds in the CI may overwrite each othersconfig.json
leading to race conditions during OCI authentication.Alternatively, all authentication with OCI registries can be overridden by setting the
TART_REGISTRY_USERNAME
andTART_REGISTRY_PASSWORD
variables. However this comes with its own set of issues, as the credentials are not associated with any host. Should a user decide to opt for a publically available image (e.g. macos-image-templates) over the internal ones, the default behaviour would have tart send internal credentials to a public OCI registry.Current workaround
The current workaround is to execute a script before tart attempts to fetch the image from the OCI registry, where we manually parse a docker-credential-store json string from the environment, select the appropriate credential and set it in tart's
TART_REGISTRY_USERNAME
/TART_REGISTRY_PASSWORD
environment variables. While this works, it is an unnecessary burden to place on larger organizations, especially if there are existing approaches for similar cases.Proposed solution
Similar to the DOCKER_CONFIG environment variable used by Docker, we could allow for setting a docker-credential-store filepath as environment variable
TART_DOCKER_CONFIG
. I chose to prefix the environment variable withTART_
to remain consistent with existing variables and to avoid conflicts with existing docker setups. tart would then evaluate the contents of the file in the same way as it currently does the~/.docker/config.json
. Since the environment variable is specific to the instance of the job being run on the machine and bound to the matching process, concurrently run image fetch operations would not run into the race condition detailed in the motivation above.Detailed design
The changes mainly revolve around the
retrieve(host:)
function in theDockerConfigCredentialsProvider
class. The external interface asCredentialProvider
remains intact, but the function internally now additionally evaluates the environment. If no configuration is available, nil is returned.The config path from the environment takes precedence over the config from default location in
~/.docker/config.json
. That is, if a config is configured through the environment and there is a config in the default location, the values from the environment config will be used. The default location config will not be considered.The config file in the default location is optional and does not produce an error if it is not present. However, if a config file was configured through the environment it is expected to be present and will produce an error if not found.
Tests
As discussed below, I added integration tests to validate the changes. To do so, the
docker_registry.py
was extended so that it can spawn an instance of the registry container which requires configurable authentication.tart.py
now allows to configure environment variables to be set for the execution of tart, enabling the injection of credentials for authentication.The following tests have been added:
Other changes
I fixed a typo in one of the test file names.