Skip to content

Commit

Permalink
feat: expose repository hosts through authenticated api call
Browse files Browse the repository at this point in the history
  • Loading branch information
at88mph committed Nov 14, 2024
1 parent 3827f6f commit 128c57a
Show file tree
Hide file tree
Showing 16 changed files with 158 additions and 42 deletions.
2 changes: 1 addition & 1 deletion deployment/helm/science-portal/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ version: 0.3.1
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.3.0"
appVersion: "0.3.1"

dependencies:
- name: "redis"
Expand Down
2 changes: 1 addition & 1 deletion deployment/helm/science-portal/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ skaha:
deployment:
hostname: example.host.com # Change this!
sciencePortal:
image: images.opencadc.org/platform/science-portal:0.3.0
image: images.opencadc.org/platform/science-portal:0.3.1
imagePullPolicy: Always

tabLabels:
Expand Down
4 changes: 2 additions & 2 deletions deployment/helm/skaha/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.9.0
version: 0.9.1

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.23.0"
appVersion: "0.23.1"

dependencies:
- name: "redis"
Expand Down
2 changes: 1 addition & 1 deletion deployment/helm/skaha/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ skahaWorkload:
deployment:
hostname: myhost.example.com # Change this!
skaha:
image: images.opencadc.org/platform/skaha:0.23.0
image: images.opencadc.org/platform/skaha:0.23.1
imagePullPolicy: Always

# Cron string for the image caching cron job schedule. Defaults to every minute.
Expand Down
2 changes: 1 addition & 1 deletion skaha/VERSION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## deployable containers have a semantic and build tag
# version tag: major.minor.patch
# build version tag: timestamp
VER=0.23.0
VER=0.23.1
TAGS="${VER} ${VER}-$(date -u +"%Y%m%dT%H%M%S")"
unset VER
6 changes: 3 additions & 3 deletions skaha/src/main/java/org/opencadc/skaha/SkahaAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
import org.opencadc.permissions.TokenTool;
import org.opencadc.permissions.WriteGrant;
import org.opencadc.skaha.image.Image;
import org.opencadc.skaha.registry.ImageRegistryAuth;
import org.opencadc.skaha.repository.ImageRepositoryAuth;
import org.opencadc.skaha.session.Session;
import org.opencadc.skaha.session.SessionDAO;
import org.opencadc.skaha.utils.CommandExecutioner;
Expand Down Expand Up @@ -280,13 +280,13 @@ protected void initRequest() throws Exception {
}
}

protected ImageRegistryAuth getRegistryAuth(final String registryHost) {
protected ImageRepositoryAuth getRegistryAuth(final String registryHost) {
final String registryAuthValue = this.syncInput.getHeader(SkahaAction.X_REGISTRY_AUTH_HEADER);
if (!StringUtil.hasText(registryAuthValue)) {
throw new IllegalArgumentException("No authentication provided for unknown or private image. Use "
+ SkahaAction.X_REGISTRY_AUTH_HEADER + " request header with base64Encode(username:secret).");
}
return ImageRegistryAuth.fromEncoded(registryAuthValue, registryHost);
return ImageRepositoryAuth.fromEncoded(registryAuthValue, registryHost);
}

private boolean isSkahaCallBackFlow(Subject currentSubject) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public void doAction() throws Exception {
super.initRequest();

File propertiesFile = ResourceContexts.getResourcesFile("k8s-resources.json");
byte[] bytes = getBytes(propertiesFile);
byte[] bytes = GetAction.getBytes(propertiesFile);
syncOutput.setHeader("Content-Type", "application/json");
syncOutput.getOutputStream().write(bytes);
}
Expand Down
43 changes: 43 additions & 0 deletions skaha/src/main/java/org/opencadc/skaha/repository/GetAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package org.opencadc.skaha.repository;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Arrays;
import org.json.JSONWriter;
import org.opencadc.skaha.SkahaAction;

/**
* Output the resource context information.
*
* @author majorb
*/
public class GetAction extends SkahaAction {

@Override
public void doAction() throws Exception {
initRequest();
final Writer writer = initWriter();
final JSONWriter jsonWriter = new JSONWriter(writer).array();
try {
Arrays.stream(getHarborHosts()).forEach(jsonWriter::value);
} finally {
jsonWriter.endArray();
writer.flush();
}
}

@Override
protected void initRequest() throws Exception {
super.initRequest();
}

Writer initWriter() throws IOException {
this.syncOutput.setHeader("content-type", "application/json");
return new OutputStreamWriter(syncOutput.getOutputStream());
}

String[] getHarborHosts() {
return this.harborHosts.toArray(new String[0]);
}
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
package org.opencadc.skaha.registry;
package org.opencadc.skaha.repository;

import ca.nrc.cadc.util.Base64;
import ca.nrc.cadc.util.StringUtil;
import java.nio.charset.StandardCharsets;

/**
* Represents credentials to an image registry. Will be used from the request input as a base64 encoded value.
* Represents credentials to an image repository. Will be used from the request input as a base64 encoded value.
*/
public class ImageRegistryAuth {
public class ImageRepositoryAuth {
private static final String VALUE_DELIMITER = ":";

private final String host;
private final String username;
private final byte[] secret;

ImageRegistryAuth(final String username, final byte[] secret, final String host) {
ImageRepositoryAuth(final String username, final byte[] secret, final String host) {
if (!StringUtil.hasText(username)) {
throw new IllegalArgumentException("username is required.");
} else if (secret.length == 0) {
throw new IllegalArgumentException("secret value cannot be empty.");
} else if (!StringUtil.hasText(host)) {
throw new IllegalArgumentException("registry host is required.");
throw new IllegalArgumentException("repository host is required.");
}

this.username = username;
Expand All @@ -29,31 +29,31 @@ public class ImageRegistryAuth {
}

/**
* Constructor to use the Base64 encoded value to obtain the credentials for an Image Registry.
* Constructor to use the Base64 encoded value to obtain the credentials for an Image Repository.
*
* @param encodedValue The Base64 encoded String. Never null.
* @param host The registry host for these credentials.
* @return ImageRegistryAuth instance. Never null.
* @param host The repository host for these credentials.
* @return ImageRepositoryAuth instance. Never null.
*/
public static ImageRegistryAuth fromEncoded(final String encodedValue, final String host) {
public static ImageRepositoryAuth fromEncoded(final String encodedValue, final String host) {
if (!StringUtil.hasText(encodedValue)) {
throw new IllegalArgumentException("Encoded auth username and key is required.");
} else if (!StringUtil.hasText(host)) {
throw new IllegalArgumentException("Registry host is required.");
throw new IllegalArgumentException("Repository host is required.");
}

final String decodedValue = new String(Base64.decode(encodedValue), StandardCharsets.UTF_8);
final String[] values = decodedValue.split(ImageRegistryAuth.VALUE_DELIMITER);
final String[] values = decodedValue.split(ImageRepositoryAuth.VALUE_DELIMITER);

if (values.length != 2) {
throw new IllegalArgumentException("Invalid input. Must be in form of username:secret");
}

return new ImageRegistryAuth(values[0].trim(), values[1].trim().getBytes(), host);
return new ImageRepositoryAuth(values[0].trim(), values[1].trim().getBytes(), host);
}

public String getEncoded() {
return Base64.encodeString(this.username + ImageRegistryAuth.VALUE_DELIMITER + new String(this.secret));
return Base64.encodeString(this.username + ImageRepositoryAuth.VALUE_DELIMITER + new String(this.secret));
}

public String getHost() {
Expand Down
10 changes: 5 additions & 5 deletions skaha/src/main/java/org/opencadc/skaha/session/PostAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
import org.opencadc.skaha.SkahaAction;
import org.opencadc.skaha.context.ResourceContexts;
import org.opencadc.skaha.image.Image;
import org.opencadc.skaha.registry.ImageRegistryAuth;
import org.opencadc.skaha.repository.ImageRepositoryAuth;
import org.opencadc.skaha.utils.CommandExecutioner;
import org.opencadc.skaha.utils.PosixCache;

Expand Down Expand Up @@ -503,8 +503,8 @@ private String validateImage(String imageID, String type) throws Exception {
// TODO: and since we can't verify one way or the other, let them through.
if (image == null) {
log.warn("Image " + imageID + " missing from cache...");
final ImageRegistryAuth imageRegistryAuth = getRegistryAuth(imageRegistryHost);
if (imageRegistryAuth == null) {
final ImageRepositoryAuth imageRepositoryAuth = getRegistryAuth(imageRegistryHost);
if (imageRepositoryAuth == null) {
throw new ResourceNotFoundException("image not found or not labelled: " + imageID);
} else {
log.warn("Assuming image " + imageID + " is private as credentials were supplied.");
Expand Down Expand Up @@ -606,7 +606,7 @@ public void createSession(
// In the absence of the existence of a public image, assume Private. The validateImage() step above will have
// caught a non-existent Image already.
if (getPublicImage(image) == null) {
final ImageRegistryAuth userRegistryAuth = getRegistryAuth(getRegistryHost(image));
final ImageRepositoryAuth userRegistryAuth = getRegistryAuth(getRegistryHost(image));
imageRegistrySecretName = createRegistryImageSecret(userRegistryAuth);
} else {
imageRegistrySecretName = PostAction.DEFAULT_SOFTWARE_IMAGESECRET_VALUE;
Expand Down Expand Up @@ -697,7 +697,7 @@ private String generateToken() throws Exception {
* @param registryAuth The credentials to use to authenticate to the Image Registry.
* @return String secret name, never null.
*/
private String createRegistryImageSecret(final ImageRegistryAuth registryAuth) throws Exception {
private String createRegistryImageSecret(final ImageRepositoryAuth registryAuth) throws Exception {
final String username = this.posixPrincipal.username;
final String secretName = "registry-auth-" + username.toLowerCase();
log.debug("Creating user secret " + secretName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import org.apache.log4j.Logger;
import org.json.JSONObject;
import org.opencadc.skaha.K8SUtil;
import org.opencadc.skaha.registry.ImageRegistryAuth;
import org.opencadc.skaha.repository.ImageRepositoryAuth;

public class CommandExecutioner {
private static final Logger log = Logger.getLogger(CommandExecutioner.class);
Expand Down Expand Up @@ -92,7 +92,7 @@ public static void execute(final String[] command, final OutputStream standardOu
* @param secretName The name of the secret to create.
* @throws Exception If there is an error creating the secret.
*/
public static void ensureRegistrySecret(final ImageRegistryAuth registryAuth, final String secretName)
public static void ensureRegistrySecret(final ImageRepositoryAuth registryAuth, final String secretName)
throws Exception {
// delete any old secret by this name
final String[] deleteCmd = CommandExecutioner.getDeleteSecretCommand(secretName);
Expand Down Expand Up @@ -135,7 +135,7 @@ static String[] getDeleteSecretCommand(final String secretName) {
return new String[] {"kubectl", "--namespace", K8SUtil.getWorkloadNamespace(), "delete", "secret", secretName};
}

static String[] getRegistryCreateSecretCommand(final ImageRegistryAuth registryAuth, final String secretName) {
static String[] getRegistryCreateSecretCommand(final ImageRepositoryAuth registryAuth, final String secretName) {
if (registryAuth == null) {
throw new IllegalArgumentException("registryAuth is required.");
} else if (!StringUtil.hasText(secretName)) {
Expand Down
15 changes: 15 additions & 0 deletions skaha/src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@
<load-on-startup>2</load-on-startup>
</servlet>

<servlet>
<servlet-name>ImageRepositoryServlet</servlet-name>
<servlet-class>ca.nrc.cadc.rest.RestServlet</servlet-class>
<init-param>
<param-name>get</param-name>
<param-value>org.opencadc.skaha.repository.GetAction</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>

<!-- Servlet that identifies the services provided by platform -->
<servlet>
<servlet-name>CapabilitiesServlet</servlet-name>
Expand Down Expand Up @@ -122,6 +132,11 @@
<servlet-name>ImageServlet</servlet-name>
<url-pattern>/v0/image/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>ImageRepositoryServlet</servlet-name>
<url-pattern>/v0/repository/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>CapabilitiesServlet</servlet-name>
Expand Down
19 changes: 19 additions & 0 deletions skaha/src/main/webapp/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,22 @@ paths:
description: Service busy
default:
description: Unexpected error
/v0/registry:
get:
description: |
List the Image Registry hosts configured as a JSON Array.
tags:
- Harbor, Registry, Image, Repository
responses:
'200':
description: Successful response
'401':
description: Not authenticated
'403':
description: Permission denied
'500':
description: Internal error
'503':
description: Service busy
default:
description: Unexpected error
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.opencadc.skaha.repository;

import java.io.StringWriter;
import java.io.Writer;
import org.json.JSONArray;
import org.junit.Assert;
import org.junit.Test;

public class GetActionTest {
@Test
public void ensureJSON() throws Exception {
final String[] hostArray = new String[] {"images.example.org", "images2.example.org"};
final Writer writer = new StringWriter();
final GetAction testSubject = new GetAction() {
@Override
protected void initRequest() {
// Do nothing.
}

@Override
Writer initWriter() {
return writer;
}

@Override
String[] getHarborHosts() {
return hostArray;
}
};

testSubject.doAction();

final JSONArray resultArray = new JSONArray(writer.toString());
final String[] resultStringArray =
resultArray.toList().stream().map(Object::toString).toArray(String[]::new);

Assert.assertArrayEquals("Wrong host array", hostArray, resultStringArray);
}
}
Loading

0 comments on commit 128c57a

Please sign in to comment.