DockerComposeYamlInstallationProvider.java

package org.keycloak.protocol.docker.installation;

import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ClientInstallationProvider;
import org.keycloak.protocol.docker.DockerAuthV2Protocol;
import org.keycloak.protocol.docker.installation.compose.DockerComposeZipContent;

import jakarta.ws.rs.core.Response;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.security.cert.Certificate;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class DockerComposeYamlInstallationProvider implements ClientInstallationProvider {
    private static Logger log = Logger.getLogger(DockerComposeYamlInstallationProvider.class);

    public static final String ROOT_DIR = "keycloak-docker-compose-yaml/";

    @Override
    public ClientInstallationProvider create(final KeycloakSession session) {
        return this;
    }

    @Override
    public void init(final Config.Scope config) {
        // no-op
    }

    @Override
    public void postInit(final KeycloakSessionFactory factory) {
        // no-op
    }

    @Override
    public void close() {
        // no-op
    }

    @Override
    public String getId() {
        return "docker-v2-compose-yaml";
    }

    @Override
    public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
        final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        final ZipOutputStream zipOutput = new ZipOutputStream(byteStream);

        try {
            return generateInstallation(zipOutput, byteStream, session.keys().getActiveRsaKey(realm).getCertificate(), session.getContext().getUri().getBaseUri().toURL(), realm.getName(), client.getClientId());
        } catch (final IOException e) {
            try {
                zipOutput.close();
            } catch (final IOException ex) {
                // do nothing, already in an exception
            }
            try {
                byteStream.close();
            } catch (final IOException ex) {
                // do nothing, already in an exception
            }
            throw new RuntimeException("Error occurred during attempt to generate docker-compose yaml installation files", e);
        }
    }

    public Response generateInstallation(final ZipOutputStream zipOutput, final ByteArrayOutputStream byteStream, final Certificate realmCert, final URL realmBaseURl,
                                         final String realmName, final String clientName) throws IOException {
        final DockerComposeZipContent zipContent = new DockerComposeZipContent(realmCert, realmBaseURl, realmName, clientName);

        zipOutput.putNextEntry(new ZipEntry(ROOT_DIR));

        // Write docker compose file
        zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "docker-compose.yaml"));
        zipOutput.write(zipContent.getYamlFile().generateDockerComposeFileBytes());
        zipOutput.closeEntry();

        // Write data directory
        zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/"));
        zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/.gitignore"));
        zipOutput.write("*".getBytes());
        zipOutput.closeEntry();

        // Write certificates
        final String certsDirectory = ROOT_DIR + zipContent.getCertsDirectory().getDirectoryName() + "/";
        zipOutput.putNextEntry(new ZipEntry(certsDirectory));
        zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostCertFile().getKey()));
        zipOutput.write(zipContent.getCertsDirectory().getLocalhostCertFile().getValue());
        zipOutput.closeEntry();
        zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostKeyFile().getKey()));
        zipOutput.write(zipContent.getCertsDirectory().getLocalhostKeyFile().getValue());
        zipOutput.closeEntry();
        zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getIdpTrustChainFile().getKey()));
        zipOutput.write(zipContent.getCertsDirectory().getIdpTrustChainFile().getValue());
        zipOutput.closeEntry();

        // Write README to .zip
        zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "README.md"));
        try (BufferedReader br = new BufferedReader(new InputStreamReader(DockerComposeYamlInstallationProvider.class.getResourceAsStream("/DockerComposeYamlReadme.md")))) {
            final String readmeContent = br.lines().collect(Collectors.joining("\n"));
            zipOutput.write(readmeContent.getBytes());
            zipOutput.closeEntry();
        }

        zipOutput.close();
        byteStream.close();

        return Response.ok(byteStream.toByteArray(), getMediaType()).build();
    }

    @Override
    public String getProtocol() {
        return DockerAuthV2Protocol.LOGIN_PROTOCOL;
    }

    @Override
    public String getDisplayType() {
        return "Docker Compose YAML";
    }

    @Override
    public String getHelpText() {
        return "Produces a zip file that can be used to stand up a development registry on localhost";
    }

    @Override
    public String getFilename() {
        return "keycloak-docker-compose-yaml.zip";
    }

    @Override
    public String getMediaType() {
        return "application/zip";
    }

    @Override
    public boolean isDownloadOnly() {
        return true;
    }
}