diff --git a/deployment/helm/science-portal/Chart.yaml b/deployment/helm/science-portal/Chart.yaml index d6ea9b0c..901cb50c 100644 --- a/deployment/helm/science-portal/Chart.yaml +++ b/deployment/helm/science-portal/Chart.yaml @@ -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" diff --git a/deployment/helm/science-portal/values.yaml b/deployment/helm/science-portal/values.yaml index 6f69ec15..9248b648 100644 --- a/deployment/helm/science-portal/values.yaml +++ b/deployment/helm/science-portal/values.yaml @@ -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: diff --git a/deployment/helm/skaha/Chart.yaml b/deployment/helm/skaha/Chart.yaml index c992b63b..13188e6f 100644 --- a/deployment/helm/skaha/Chart.yaml +++ b/deployment/helm/skaha/Chart.yaml @@ -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" diff --git a/deployment/helm/skaha/values.yaml b/deployment/helm/skaha/values.yaml index bcf4c712..6c83afb0 100644 --- a/deployment/helm/skaha/values.yaml +++ b/deployment/helm/skaha/values.yaml @@ -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. diff --git a/skaha/VERSION b/skaha/VERSION index 64f76ae5..7592d8bb 100644 --- a/skaha/VERSION +++ b/skaha/VERSION @@ -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 diff --git a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java index 17659e62..d0da3bb8 100644 --- a/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/SkahaAction.java @@ -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; @@ -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) { diff --git a/skaha/src/main/java/org/opencadc/skaha/context/GetAction.java b/skaha/src/main/java/org/opencadc/skaha/context/GetAction.java index 123f2f6b..c82749d4 100644 --- a/skaha/src/main/java/org/opencadc/skaha/context/GetAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/context/GetAction.java @@ -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); } diff --git a/skaha/src/main/java/org/opencadc/skaha/repository/GetAction.java b/skaha/src/main/java/org/opencadc/skaha/repository/GetAction.java new file mode 100644 index 00000000..a13c9951 --- /dev/null +++ b/skaha/src/main/java/org/opencadc/skaha/repository/GetAction.java @@ -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]); + } +} diff --git a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java b/skaha/src/main/java/org/opencadc/skaha/repository/ImageRepositoryAuth.java similarity index 60% rename from skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java rename to skaha/src/main/java/org/opencadc/skaha/repository/ImageRepositoryAuth.java index fba295c2..cfd09d16 100644 --- a/skaha/src/main/java/org/opencadc/skaha/registry/ImageRegistryAuth.java +++ b/skaha/src/main/java/org/opencadc/skaha/repository/ImageRepositoryAuth.java @@ -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; @@ -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() { diff --git a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java index 38453e73..8e36434e 100644 --- a/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java +++ b/skaha/src/main/java/org/opencadc/skaha/session/PostAction.java @@ -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; @@ -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."); @@ -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; @@ -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); diff --git a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java index 4a093e2a..82243002 100644 --- a/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java +++ b/skaha/src/main/java/org/opencadc/skaha/utils/CommandExecutioner.java @@ -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); @@ -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); @@ -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)) { diff --git a/skaha/src/main/webapp/WEB-INF/web.xml b/skaha/src/main/webapp/WEB-INF/web.xml index 9c0ae7e9..dc1c533d 100644 --- a/skaha/src/main/webapp/WEB-INF/web.xml +++ b/skaha/src/main/webapp/WEB-INF/web.xml @@ -73,6 +73,16 @@ 2 + + ImageRepositoryServlet + ca.nrc.cadc.rest.RestServlet + + get + org.opencadc.skaha.repository.GetAction + + 2 + + CapabilitiesServlet @@ -122,6 +132,11 @@ ImageServlet /v0/image/* + + + ImageRepositoryServlet + /v0/repository/* + CapabilitiesServlet diff --git a/skaha/src/main/webapp/service.yaml b/skaha/src/main/webapp/service.yaml index 5cbb57c4..0b2a004c 100644 --- a/skaha/src/main/webapp/service.yaml +++ b/skaha/src/main/webapp/service.yaml @@ -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 diff --git a/skaha/src/test/java/org/opencadc/skaha/repository/GetActionTest.java b/skaha/src/test/java/org/opencadc/skaha/repository/GetActionTest.java new file mode 100644 index 00000000..abd6bd33 --- /dev/null +++ b/skaha/src/test/java/org/opencadc/skaha/repository/GetActionTest.java @@ -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); + } +} diff --git a/skaha/src/test/java/org/opencadc/skaha/registry/ImageRegistryAuthTest.java b/skaha/src/test/java/org/opencadc/skaha/repository/ImageRepositoryAuthTest.java similarity index 73% rename from skaha/src/test/java/org/opencadc/skaha/registry/ImageRegistryAuthTest.java rename to skaha/src/test/java/org/opencadc/skaha/repository/ImageRepositoryAuthTest.java index 4ae6e08a..1b7852c5 100644 --- a/skaha/src/test/java/org/opencadc/skaha/registry/ImageRegistryAuthTest.java +++ b/skaha/src/test/java/org/opencadc/skaha/repository/ImageRepositoryAuthTest.java @@ -1,32 +1,32 @@ -package org.opencadc.skaha.registry; +package org.opencadc.skaha.repository; import java.nio.charset.StandardCharsets; import java.util.Base64; import org.junit.Assert; import org.junit.Test; -public class ImageRegistryAuthTest { +public class ImageRepositoryAuthTest { @Test public void testFromEncodedBadInputs() { try { - ImageRegistryAuth.fromEncoded(null, "host"); + ImageRepositoryAuth.fromEncoded(null, "host"); Assert.fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { Assert.assertEquals("Encoded auth username and key is required.", e.getMessage()); } try { - ImageRegistryAuth.fromEncoded("", "host"); + ImageRepositoryAuth.fromEncoded("", "host"); Assert.fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { Assert.assertEquals("Encoded auth username and key is required.", e.getMessage()); } try { - ImageRegistryAuth.fromEncoded("value", null); + ImageRepositoryAuth.fromEncoded("value", null); Assert.fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { - Assert.assertEquals("Registry host is required.", e.getMessage()); + Assert.assertEquals("Repository host is required.", e.getMessage()); } } @@ -34,7 +34,7 @@ public void testFromEncodedBadInputs() { public void testFromEncoded() { final String encodedValue = new String(Base64.getEncoder().encode("username:supersecret".getBytes(StandardCharsets.UTF_8))); - final ImageRegistryAuth auth = ImageRegistryAuth.fromEncoded(encodedValue, "host.example.com"); + final ImageRepositoryAuth auth = ImageRepositoryAuth.fromEncoded(encodedValue, "host.example.com"); Assert.assertEquals("Wrong username", "username", auth.getUsername()); Assert.assertArrayEquals("Wrong secret", "supersecret".getBytes(), auth.getSecret()); diff --git a/skaha/src/test/java/org/opencadc/skaha/utils/CommandExecutionerTest.java b/skaha/src/test/java/org/opencadc/skaha/utils/CommandExecutionerTest.java index 7447e0f3..dd04950c 100644 --- a/skaha/src/test/java/org/opencadc/skaha/utils/CommandExecutionerTest.java +++ b/skaha/src/test/java/org/opencadc/skaha/utils/CommandExecutionerTest.java @@ -4,7 +4,7 @@ import org.junit.Assert; import org.junit.Test; import org.opencadc.skaha.K8SUtil; -import org.opencadc.skaha.registry.ImageRegistryAuth; +import org.opencadc.skaha.repository.ImageRepositoryAuth; public class CommandExecutionerTest { @Test @@ -31,17 +31,17 @@ public void testGetRegistryCreateSecretCommand() { Assert.assertEquals("registryAuth is required.", e.getMessage()); } - final ImageRegistryAuth imageRegistryAuth = ImageRegistryAuth.fromEncoded( + final ImageRepositoryAuth imageRepositoryAuth = ImageRepositoryAuth.fromEncoded( new String(Base64.getEncoder().encode("username:password".getBytes())), "host"); try { - CommandExecutioner.getRegistryCreateSecretCommand(imageRegistryAuth, ""); + CommandExecutioner.getRegistryCreateSecretCommand(imageRepositoryAuth, ""); } catch (IllegalArgumentException e) { Assert.assertEquals("secretName is required.", e.getMessage()); } try { - CommandExecutioner.getRegistryCreateSecretCommand(imageRegistryAuth, null); + CommandExecutioner.getRegistryCreateSecretCommand(imageRepositoryAuth, null); } catch (IllegalArgumentException e) { Assert.assertEquals("secretName is required.", e.getMessage()); }