public IRegistry GetRegistry(RegistryCredentials credentials) { credentials.Registry = credentials.Registry.ToLowerInvariant(); if (Registry.DockerHubAliases.Contains(credentials.Registry)) { credentials.Registry = Registry.DockerHub; } return(new Registry(credentials, Settings)); }
public ActionResult RegistryCredentials(RegistryCredentials regCreds) { try { _protectorService.ProtectAndStore(regCreds.Registry, regCreds); return(Ok()); } catch (Exception ex) { return(BadRequest("Couldn't store credentials: " + ex.Message)); } }
public async Task <IActionResult> Post([FromBody] RegistryCredentials credentials) { // must specify a registry if (string.IsNullOrEmpty(credentials.Registry)) { return(Unauthorized()); } // deny requests for foreign instances, if configured if (!string.IsNullOrEmpty(Config.Registry) && credentials.Registry.ToLowerInvariant() != Config.Registry.ToLowerInvariant()) { return(Unauthorized()); } try { credentials.Registry = RegistryCredentials.DeAliasDockerHub(credentials.Registry); var handler = new AuthHandler(cache, Config, loggerFactory.CreateLogger <AuthHandler>()); await handler.LoginAsync(credentials.Registry, credentials.Username, credentials.Password); var json = JsonConvert.SerializeObject(credentials); // publicly visible parameters for session validation var headers = new Dictionary <string, object> { { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() }, { "exp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() + Config.AuthTokenLifetime }, { "reg", credentials.Registry } }; var token = new Token { Usr = credentials.Username, Pwd = credentials.Password, Reg = credentials.Registry, Iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Exp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + Config.AuthTokenLifetime }; var jwe = Jose.JWT.Encode(token, crypto, JweAlgorithm.RSA_OAEP, JweEncryption.A256GCM, extraHeaders: headers); return(Ok(new { token = jwe })); } catch (Exception ex) { Logger.LogError(ex, "Error authenticating token request."); return(Unauthorized()); } }
public static ClaimsIdentity ToClaimsIdentity(this RegistryCredentials credentials) { var claims = new List <Claim>(); claims.Add(new Claim(nameof(RegistryCredentials.Registry), credentials.Registry, ClaimValueTypes.String)); if (!string.IsNullOrEmpty(credentials.Username)) { claims.Add(new Claim(nameof(RegistryCredentials.Username), credentials.Username, ClaimValueTypes.String)); } if (!string.IsNullOrEmpty(credentials.Password)) { claims.Add(new Claim(nameof(RegistryCredentials.Password), credentials.Password, ClaimValueTypes.String)); } var identity = new ClaimsIdentity(claims, "RegistryCredentials"); return(identity); }
public Task <AuthenticateResult> AuthenticateAsync(string authorization) { try { var header = AuthenticationHeaderValue.Parse(authorization); logger.LogTrace($"Got authorization header: {header}"); var jweHeader = Jose.JWT.Headers(header.Parameter); logger.LogDebug($"JWE Header: {string.Join(", ", jweHeader.Select(p => string.Join(": ", p.Key, p.Value)))}"); if (!jweHeader.ContainsKey("exp") || (long)jweHeader["exp"] <= DateTimeOffset.UtcNow.ToUnixTimeSeconds()) { return(Task.FromResult(AuthenticateResult.Fail("{ \"error\": \"The token has expired\" }"))); } var token = Jose.JWT.Decode <Token>(header.Parameter, crypto); var credentials = new RegistryCredentials { Password = token.Pwd, Username = token.Usr, Registry = token.Reg }; logger.LogDebug($"Decoded token for {credentials.Registry}"); if (!string.IsNullOrEmpty(config.Registry) && RegistryCredentials.DeAliasDockerHub(config.Registry.ToLowerInvariant()) != credentials.Registry.ToLowerInvariant()) { return(Task.FromResult(AuthenticateResult.Fail("{ error: \"The supplied token is for an unsupported registry.\" }"))); } var principal = new ClaimsPrincipal(credentials.ToClaimsIdentity()); var ticket = new AuthenticationTicket(principal, "token"); return(Task.FromResult(AuthenticateResult.Success(ticket))); } catch (Exception ex) { logger.LogError(ex, "Token authentication failed."); return(Task.FromResult(AuthenticateResult.Fail("{ \"error\": \"The supplied token is invalid.\" }"))); } }
public IActionResult Post([FromBody] RegistryCredentials credentials) { // must specify a registry if (string.IsNullOrEmpty(credentials.Registry)) { return(Unauthorized()); } // deny requests for foreign instances, if configured if (!string.IsNullOrEmpty(Config.Catalog?.Registry) && credentials.Registry.ToLowerInvariant() != Config.Catalog.Registry.ToLowerInvariant()) { return(Unauthorized()); } try { var handler = new AuthHandler(_Cache); handler.Login(credentials.Registry, credentials.Username, credentials.Password); var json = JsonConvert.SerializeObject(credentials); var cipherText = _Crypto.Encrypt(json); return(Ok(new { token = Jose.JWT.Encode(new Token { Crd = cipherText, Usr = credentials.Username, Reg = credentials.Registry, Iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Exp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + Config.Security.TokenLifetime }, _Crypto.ToDotNetRSA(), Jose.JwsAlgorithm.RS256) })); } catch (Exception ex) { Logger.LogError(ex, "Error authenticating token request."); return(Unauthorized()); } }
public PullImageTask(string fqin, string tag, RegistryCredentials regCreds) { _fqin = fqin; _tag = tag; _regCreds = regCreds; }
public override async Task DoRequestAsync(IndexRequest request) { try { RegistryCredentials credentials = null; // if the request was submitted by a user, it must have auth info included if (!string.IsNullOrEmpty(request.Authorization)) { var authResult = authDecoder.AuthenticateAsync(request.Authorization).Result; if (authResult.Succeeded) { credentials = authResult.Principal.ToRegistryCredentials(); } } // if the request came via an event sink, there is no auth provided, and we need to have a default user configured else { credentials = config.GetCatalogCredentials() ?? throw new ArgumentException("The indexing request had no included authorization, and no default catalog user is configured."); } if (credentials == null) { logger.LogWarning("Authorization failed for the work item. A token may have expired since it was first submitted."); } else { await authHandler.LoginAsync(credentials); var client = clientFactory.GetClient(authHandler); // if deep indexing is configured, ignore target paths if (config.DeepIndexing) { request.TargetPaths = new string[0]; } var imageSet = await client.GetImageSetAsync(request.TargetRepo, request.TargetDigest); if ((imageSet?.Images?.Count() ?? 0) != 1) { throw new Exception($"Couldn't find a valid image for {request.TargetRepo}:{request.TargetDigest}"); } var image = imageSet.Images.First(); using (var @lock = await cacheFactory.Get <object>().TakeLockAsync($"idx:{image.Digest}", TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5))) { if (!indexStore.IndexExists(image.Digest, request.TargetPaths.ToArray())) { logger.LogInformation($"Starting index for {request.TargetRepo}:{request.TargetDigest}"); var indexes = client.GetIndexes(request.TargetRepo, image, request.TargetPaths.ToArray()); indexStore.SetIndex(indexes, image.Digest, request.TargetPaths.ToArray()); logger.LogInformation($"Completed indexing {indexes.Max(i => i.Depth)} layer(s) from {request.TargetRepo}:{request.TargetDigest} {(request.TargetPaths.Count() == 0 ? "" : $"({string.Join(", ", request.TargetPaths)})")}"); } else { logger.LogInformation($"Index already exists for {request.TargetRepo}:{request.TargetDigest}"); } } } } catch (Exception ex) { logger.LogError(ex, $"Processing failed for work item\n {Newtonsoft.Json.JsonConvert.SerializeObject(request)}"); } }
/// <summary> /// Submits all layers of an image to Clair for scanning. If a layer has been scanned previously, it will be skipped. /// Repository info is necessary to download blobs for scanning, but once scanned from any repository blobs do not need to /// be rescanned/analyzed. Calling GetScan(hard: true) will always return the most current available vulnerability analysis. /// </summary> /// <param name="registry"></param> /// <param name="repository"></param> /// <param name="image"></param> public void RequestScan(string repository, Image image, string host, string authorization) { var cache = cacheFactory.Get <Result>(); var lockTime = new TimeSpan(0, 5, 0); // if this image is already in cache, skip it entirely if (!cache.ExistsAsync(GetKey(image)).Result) { bool layerErrors = false; string lastError = string.Empty; using (var scanlock = cache.TakeLockAsync($"scan:{image.Digest}", lockTime, lockTime).Result) { Layer previousLayer = null; foreach (var layer in image.Layers.Reverse()) { if (!CheckLayerScanned(layer)) { var uri = new Uri(RegistryCredentials.HostToEndpoint(host, $"{repository}/blobs/{layer.Digest}")); var request = new ClairLayerRequest { Layer = new ClairLayerRequest.LayerRequest { Name = layer.Digest, ParentName = previousLayer?.Digest, Path = uri.ToString(), Headers = string.IsNullOrEmpty(authorization) ? null : new ClairLayerRequest.LayerRequest.LayerRequestHeaders { Authorization = authorization } } }; try { var tokenSource = new CancellationTokenSource(); tokenSource.CancelAfter(60000); clair.SubmitLayer(request, tokenSource.Token).Wait(); } catch (AggregateException ex) { if (ex.InnerException is ApiException && ((ApiException)ex.InnerException).StatusCode == (System.Net.HttpStatusCode) 422) { // at least one layer had issues, which may or may not have invalidated the scan // this can be transient, it can be a false error, or it can genuinely mean the image is currently unscannable // https://github.com/coreos/clair/issues/543 layerErrors = true; var errorContent = (ex.InnerException as ApiException).Content; try { var json = JObject.Parse(errorContent); lastError = (string)json["Error"]["Message"]; } catch { logger.LogError(ex, $"Could not parse Clair error response '{errorContent}'"); } continue; } else { throw; } } } previousLayer = layer; } // if any layers failed above, check that we can get a valid result, and if not set an error entry so we can avoid further attempts for now if (layerErrors && !CheckLayerScanned(image.Layers.First())) { cache.SetAsync(GetKey(image), new Result { Status = RequestStatus.Failed, Message = lastError ?? "At least one layer of the image could not be scanned." }).Wait(); } } } }
public override async Task DoRequestAsync(ScanRequest request) { try { RegistryCredentials credentials = null; // if the request was submitted by a user, it must have auth info included if (!string.IsNullOrEmpty(request.Authorization)) { var authResult = authDecoder.AuthenticateAsync(request.Authorization).Result; if (authResult.Succeeded) { credentials = authResult.Principal.ToRegistryCredentials(); } } // if the request came via an event sink, there is no auth provided, and we need to have a default user configured else { credentials = config.GetCatalogCredentials() ?? throw new ArgumentException("The indexing request had no included authorization, and no default catalog user is configured."); } if (credentials == null) { logger.LogError("Authorization failed for the work item. A token may have expired since it was first submitted."); } else { await authHandler.LoginAsync(credentials); var scope = authHandler.RepoPullScope(request.TargetRepo); if (await authHandler.AuthorizeAsync(scope)) { var proxyAuth = authHandler.TokensRequired ? $"Bearer {(await authHandler.GetAuthorizationAsync(scope)).Parameter}" : string.Empty; var client = clientFactory.GetClient(authHandler); var imageSet = await client.GetImageSetAsync(request.TargetRepo, request.TargetDigest); if ((imageSet?.Images?.Count() ?? 0) != 1) { throw new Exception($"Couldn't find a valid image for {request.TargetRepo}:{request.TargetDigest}"); } var scanResult = scanner.GetScan(imageSet.Images.First()); if (scanResult == null) { if (request.Submitted) { // we've already submitted this one to the scanner, just sleep on it for a few seconds Thread.Sleep(2000); } else { var host = authHandler.GetRegistryHost(); scanner.RequestScan(request.TargetRepo, imageSet.Images.First(), host, proxyAuth); logger.LogInformation($"Submitted {request.TargetRepo}:{request.TargetDigest} to {scanner.GetType().Name} for analysis."); request.Submitted = true; } queue.Push(request); } else { logger.LogInformation($"Got latest {scanner.GetType().Name} scan for {request.TargetRepo}:{request.TargetDigest}"); } } else { logger.LogError($"Failed to get pull authorization for {request.TargetRepo}"); } } } catch (Exception ex) { logger.LogError(ex, $"Processing failed for work item\n {Newtonsoft.Json.JsonConvert.SerializeObject(request)}"); } }
/// <inheritdoc/> public void Run(ModuleContext context) { var hive = HiveHelper.Hive; string hostname; if (!context.ValidateArguments(context.Arguments, validModuleArgs)) { context.Failed = true; return; } // Obtain common arguments. context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [state]"); if (!context.Arguments.TryGetValue <string>("state", out var state)) { state = "present"; } state = state.ToLowerInvariant(); if (context.HasErrors) { return; } var manager = hive.GetReachableManager(); var sbErrorNodes = new StringBuilder(); // Determine whether the registry service is already deployed and // also retrieve the registry credentials from Vault if present. // Note that the current registry hostname will be persisted to // Consul at [neon/service/neon-registry/hostname] when a registry // is deployed. context.WriteLine(AnsibleVerbosity.Trace, $"Inspecting the [neon-registry] service."); var currentService = hive.Docker.InspectService("neon-registry"); context.WriteLine(AnsibleVerbosity.Trace, $"Getting current registry hostname from Consul."); var currentHostname = hive.Registry.GetLocalHostname(); var currentSecret = hive.Registry.GetLocalSecret(); var currentImage = currentService?.Spec.TaskTemplate.ContainerSpec.ImageWithoutSHA; var currentCredentials = // Set blank properties for the change detection below. new RegistryCredentials() { Registry = string.Empty, Username = string.Empty, Password = string.Empty }; if (!string.IsNullOrEmpty(currentHostname)) { context.WriteLine(AnsibleVerbosity.Trace, $"Reading existing registry credentials for [{currentHostname}]."); currentCredentials = hive.Registry.GetCredentials(currentHostname); if (currentCredentials != null) { context.WriteLine(AnsibleVerbosity.Info, $"Registry credentials for [{currentHostname}] exist."); } else { context.WriteLine(AnsibleVerbosity.Info, $"Registry credentials for [{currentHostname}] do not exist."); } } // Obtain the current registry TLS certificate (if any). var currentCertificate = hive.Certificate.Get("neon-registry"); // Perform the operation. switch (state) { case "absent": context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [hostname]"); if (!context.Arguments.TryGetValue <string>("hostname", out hostname)) { throw new ArgumentException($"[hostname] module argument is required."); } if (currentService == null) { context.WriteLine(AnsibleVerbosity.Important, "[neon-registry] is not currently deployed."); } if (context.CheckMode) { context.WriteLine(AnsibleVerbosity.Important, $"Local registry will be removed when CHECK-MODE is disabled."); return; } if (currentService == null) { return; // Nothing to do } context.Changed = true; // Logout of the registry. if (currentCredentials != null) { context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry."); hive.Registry.Logout(currentHostname); } // Delete the [neon-registry] service and volume. Note that // the volume should exist on all of the manager nodes. context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] service."); manager.DockerCommand(RunOptions.None, "docker", "service", "rm", "neon-registry"); context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] volumes."); var volumeRemoveActions = new List <Action>(); var volumeRetryPolicy = new LinearRetryPolicy(typeof(TransientException), maxAttempts: 10, retryInterval: TimeSpan.FromSeconds(2)); foreach (var node in hive.Managers) { volumeRemoveActions.Add( () => { // $hack(jeff.lill): // // Docker service removal appears to be synchronous but the removal of the // actual service task containers is not. We're going to detect this and // throw a [TransientException] and then retry. using (var clonedNode = node.Clone()) { lock (context) { context.WriteLine(AnsibleVerbosity.Trace, $"Removing [neon-registry] volume on [{clonedNode.Name}]."); } volumeRetryPolicy.InvokeAsync( async() => { var response = clonedNode.DockerCommand(RunOptions.None, "docker", "volume", "rm", "neon-registry"); if (response.ExitCode != 0) { var message = $"Error removing [neon-registry] volume from [{clonedNode.Name}: {response.ErrorText}"; lock (syncLock) { context.WriteLine(AnsibleVerbosity.Info, message); } if (response.AllText.Contains("volume is in use")) { throw new TransientException(message); } } else { lock (context) { context.WriteLine(AnsibleVerbosity.Trace, $"Removed [neon-registry] volume on [{clonedNode.Name}]."); } } await Task.Delay(0); }).Wait(); } }); } NeonHelper.WaitForParallel(volumeRemoveActions); // Remove the traffic manager rule and certificate. context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] traffic manager rule."); hive.PublicTraffic.RemoveRule("neon-registry"); context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] traffic manager certificate."); hive.Certificate.Remove("neon-registry"); // Remove any related Consul state. context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] Consul [hostname] and [secret]."); hive.Registry.SetLocalHostname(null); hive.Registry.SetLocalSecret(null); // Logout the hive from the registry. context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry."); hive.Registry.Logout(currentHostname); // Remove the hive DNS host entry. context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [{currentHostname}] registry DNS hosts entry."); hive.Dns.Remove(hostname); break; case "present": if (!hive.Definition.HiveFS.Enabled) { context.WriteErrorLine("The local registry service requires hive CephFS."); return; } // Parse the [hostname], [certificate], [username] and [password] arguments. context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [hostname]"); if (!context.Arguments.TryGetValue <string>("hostname", out hostname)) { throw new ArgumentException($"[hostname] module argument is required."); } context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [certificate]"); if (!context.Arguments.TryGetValue <string>("certificate", out var certificatePem)) { throw new ArgumentException($"[certificate] module argument is required."); } if (!TlsCertificate.TryParse(certificatePem, out var certificate)) { throw new ArgumentException($"[certificate] is not a valid certificate."); } context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [username]"); if (!context.Arguments.TryGetValue <string>("username", out var username)) { throw new ArgumentException($"[username] module argument is required."); } context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [password]"); if (!context.Arguments.TryGetValue <string>("password", out var password)) { throw new ArgumentException($"[password] module argument is required."); } context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [secret]"); if (!context.Arguments.TryGetValue <string>("secret", out var secret) || string.IsNullOrEmpty(secret)) { throw new ArgumentException($"[secret] module argument is required."); } context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [image]"); if (!context.Arguments.TryGetValue <string>("image", out var image)) { image = HiveConst.NeonProdRegistry + "/neon-registry:latest"; } // Detect service changes. var hostnameChanged = hostname != currentCredentials?.Registry; var usernameChanged = username != currentCredentials?.Username; var passwordChanged = password != currentCredentials?.Password; var secretChanged = secret != currentSecret; var imageChanged = image != currentImage; var certificateChanged = certificate?.CombinedPemNormalized != currentCertificate?.CombinedPemNormalized; var updateRequired = hostnameChanged || usernameChanged || passwordChanged || secretChanged || imageChanged || certificateChanged; if (hostnameChanged) { context.WriteLine(AnsibleVerbosity.Info, $"[hostname] changed from [{currentCredentials?.Registry}] --> [{hostname}]"); } if (usernameChanged) { context.WriteLine(AnsibleVerbosity.Info, $"[username] changed from [{currentCredentials?.Username}] --> [{username}]"); } if (usernameChanged) { context.WriteLine(AnsibleVerbosity.Info, $"[password] changed from [{currentCredentials?.Password}] --> [**REDACTED**]"); } if (secretChanged) { context.WriteLine(AnsibleVerbosity.Info, $"[secret] changed from [{currentSecret}] --> [**REDACTED**]"); } if (imageChanged) { context.WriteLine(AnsibleVerbosity.Info, $"[image] changed from [{currentImage}] --> [{image}]"); } if (certificateChanged) { var currentCertRedacted = currentCertificate != null ? "**REDACTED**" : "**NONE**"; context.WriteLine(AnsibleVerbosity.Info, $"[certificate] changed from [{currentCertRedacted}] --> [**REDACTED**]"); } // Handle CHECK-MODE. if (context.CheckMode) { if (currentService == null) { context.WriteLine(AnsibleVerbosity.Important, $"Local registry will be deployed when CHECK-MODE is disabled."); return; } if (updateRequired) { context.WriteLine(AnsibleVerbosity.Important, $"One or more of the arguments have changed so the registry will be updated when CHECK-MODE is disabled."); return; } return; } // Create the hive DNS host entry we'll use to redirect traffic targeting the registry // hostname to the hive managers. We need to do this because registry IP addresses // are typically public, typically targeting the external firewall or load balancer // interface. // // The problem is that hive nodes will generally be unable to connect to the // local managers through the firewall/load balancer because most network routers // block network traffic that originates from inside the hive, then leaves // to hit the external router interface with the expectation of being routed // back inside. I believe this is an anti-spoofing security measure. var dnsRedirect = GetRegistryDnsEntry(hostname); // Perform the operation. if (currentService == null) { context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service needs to be created."); context.Changed = true; // The registry service isn't running, so we'll do a full deployment. context.WriteLine(AnsibleVerbosity.Trace, $"Setting certificate."); hive.Certificate.Set("neon-registry", certificate); context.WriteLine(AnsibleVerbosity.Trace, $"Updating Consul settings."); hive.Registry.SetLocalHostname(hostname); hive.Registry.SetLocalSecret(secret); context.WriteLine(AnsibleVerbosity.Trace, $"Adding hive DNS host entry for [{hostname}]."); hive.Dns.Set(dnsRedirect, waitUntilPropagated: true); context.WriteLine(AnsibleVerbosity.Trace, $"Writing traffic manager rule."); hive.PublicTraffic.SetRule(GetRegistryTrafficManagerRule(hostname)); context.WriteLine(AnsibleVerbosity.Trace, $"Creating the [neon-registry] service."); var createResponse = manager.DockerCommand(RunOptions.None, "docker service create", "--name", "neon-registry", "--mode", "global", "--constraint", "node.role==manager", "--env", $"USERNAME={username}", "--env", $"PASSWORD={password}", "--env", $"SECRET={secret}", "--env", $"LOG_LEVEL=info", "--env", $"READ_ONLY=false", "--mount", "type=volume,src=neon-registry,volume-driver=neon,dst=/var/lib/neon-registry", "--network", "neon-public", "--restart-delay", "10s", image); if (createResponse.ExitCode != 0) { context.WriteErrorLine($"[neon-registry] service create failed: {createResponse.ErrorText}"); return; } context.WriteLine(AnsibleVerbosity.Trace, $"Service created."); context.WriteLine(AnsibleVerbosity.Trace, $"Wait for [neon-registry] service to stabilize (30s)."); Thread.Sleep(TimeSpan.FromSeconds(30)); context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive into the [{hostname}] registry."); hive.Registry.Login(hostname, username, password); } else if (updateRequired) { context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service update is required."); context.Changed = true; // Update the service and related settings as required. if (certificateChanged) { context.WriteLine(AnsibleVerbosity.Trace, $"Updating certificate."); hive.Certificate.Set("neon-registry", certificate); } if (hostnameChanged) { context.WriteLine(AnsibleVerbosity.Trace, $"Updating traffic manager rule."); hive.PublicTraffic.SetRule(GetRegistryTrafficManagerRule(hostname)); context.WriteLine(AnsibleVerbosity.Trace, $"Updating hive DNS host entry for [{hostname}] (60 seconds)."); hive.Dns.Set(dnsRedirect, waitUntilPropagated: true); context.WriteLine(AnsibleVerbosity.Trace, $"Updating local hive hostname [{hostname}]."); hive.Registry.SetLocalHostname(hostname); if (!string.IsNullOrEmpty(currentHostname)) { context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry."); hive.Registry.Logout(currentHostname); } } if (secretChanged) { context.WriteLine(AnsibleVerbosity.Trace, $"Updating local hive secret."); hive.Registry.SetLocalSecret(secret); } context.WriteLine(AnsibleVerbosity.Trace, $"Updating service."); var updateResponse = manager.DockerCommand(RunOptions.None, "docker service update", "--env-add", $"USERNAME={username}", "--env-add", $"PASSWORD={password}", "--env-add", $"SECRET={secret}", "--env-add", $"LOG_LEVEL=info", "--env-add", $"READ_ONLY=false", "--image", image, "neon-registry"); if (updateResponse.ExitCode != 0) { context.WriteErrorLine($"[neon-registry] service update failed: {updateResponse.ErrorText}"); return; } context.WriteLine(AnsibleVerbosity.Trace, $"Service updated."); context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive into the [{hostname}] registry."); hive.Registry.Login(hostname, username, password); } else { context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service update is not required but we're logging all nodes into [{hostname}] to ensure hive consistency."); hive.Registry.Login(hostname, username, password); context.Changed = false; } break; case "prune": if (currentService == null) { context.WriteLine(AnsibleVerbosity.Important, "Registry service is not running."); return; } if (context.CheckMode) { context.WriteLine(AnsibleVerbosity.Important, "Registry will be pruned when CHECK-MODE is disabled."); return; } context.Changed = true; // Always set this to TRUE for prune. // We're going to upload a script to one of the managers that handles // putting the [neon-registry] service into READ-ONLY mode, running // the garbage collection container and then restoring [neon-registry] // to READ/WRITE mode. // // The nice thing about this is that the operation will continue to // completion on the manager node even if we lose the SSH connection. var updateScript = $@"#!/bin/bash # Update [neon-registry] to READ-ONLY mode: docker service update --env-rm READ_ONLY --env-add READ_ONLY=true neon-registry # Prune the registry: docker run \ --name neon-registry-prune \ --restart-condition=none \ --mount type=volume,src=neon-registry,volume-driver=neon,dst=/var/lib/neon-registry \ {HiveConst.NeonProdRegistry}/neon-registry garbage-collect # Restore [neon-registry] to READ/WRITE mode: docker service update --env-rm READ_ONLY --env-add READ_ONLY=false neon-registry "; var bundle = new CommandBundle("./collect.sh"); bundle.AddFile("collect.sh", updateScript, isExecutable: true); context.WriteLine(AnsibleVerbosity.Info, "Registry prune started."); var pruneResponse = manager.SudoCommand(bundle, RunOptions.None); if (pruneResponse.ExitCode != 0) { context.WriteErrorLine($"The prune operation failed. The registry may be running in READ-ONLY mode: {pruneResponse.ErrorText}"); return; } context.WriteLine(AnsibleVerbosity.Info, "Registry prune completed."); break; default: throw new ArgumentException($"[state={state}] is not one of the valid choices: [present], [absent], or [prune]."); } }
public string GetRegistryEndpoint(bool ignoreAliases = false) => RegistryCredentials.HostToEndpoint(GetRegistryHost(ignoreAliases));
/// <summary> /// Initializes the set of authorizations for this auth handler, and validates credentials with the registry service. /// </summary> public async Task LoginAsync(RegistryCredentials credentials) { AnonymousMode = false; RegistryCredentials = credentials; var host = GetRegistryHost(config.IgnoreInternalAlias); var endpoint = GetRegistryEndpoint(config.IgnoreInternalAlias); var key = GetKey(null, granted: true); logger.LogDebug($"Logging in to {endpoint} ({host})"); if (await authCache.ExistsAsync(key)) { logger.LogDebug($"Found cached credentials ({key})"); var authorization = await authCache.GetAsync(key); Service = authorization.Service; Realm = authorization.Realm; AnonymousMode = authorization.Anonymous; DockerHub = authorization.DockerHub; } else { using (var client = new HttpClient()) { AnonymousMode = (string.IsNullOrEmpty(username) && string.IsNullOrEmpty(password)); if (AnonymousMode) { logger.LogDebug("Client is in anon mode."); } // if this is a docker hub client, we can't really validate until we have a real resource to fetch, // but we can set some known values if (host == RegistryCredentials.DockerHub) { DockerHub = true; Realm = RegistryCredentials.DockerRealm; Service = RegistryCredentials.DockerService; return; } // first try an anonymous get for the registry catalog - if that succeeds but we're not in anonymous mode, throw var preLogin = await client.GetAsync(endpoint + "/"); if (preLogin.IsSuccessStatusCode) { if (!AnonymousMode) { throw new AuthenticationException("A username or password was specified, but the registry server does not support authentication."); } else { // we're anon, and an anon catalog fetch worked, so we should be golden return; } } else if (preLogin.StatusCode == HttpStatusCode.Unauthorized) { var challenge = ParseWwwAuthenticate(preLogin.Headers.WwwAuthenticate.First()); Service = challenge.service; Realm = challenge.realm; if (await UpdateAuthorizationAsync(null)) { client.DefaultRequestHeaders.Authorization = await GetAuthorizationAsync(null); var confirmation = client.GetAsync(endpoint + "/").Result; if (!confirmation.IsSuccessStatusCode) { throw new AuthenticationException($"Authentication succeded against the realm '{Realm}', but the registry returned status code '{confirmation.StatusCode}'"); } else { await authCache.SetAsync(key, new Authorization { JWT = (await GetAuthorizationAsync(null)).Parameter, Realm = Realm, Service = Service, Anonymous = AnonymousMode }, AuthTtl); } } else { throw new AuthenticationException($"Authentication failed."); } } else { throw new AuthenticationException($"The registry server returned an unexpected result code: {preLogin.StatusCode}"); } } } }