All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.projectnessie.nessie.cli.commands.ConnectCommand Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2024 Dremio
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.projectnessie.nessie.cli.commands;

import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_AUTH_TOKEN;
import static org.projectnessie.client.NessieConfigConstants.CONF_NESSIE_AUTH_TYPE;
import static org.projectnessie.client.http.impl.HttpUtils.checkArgument;
import static org.projectnessie.nessie.cli.cli.BaseNessieCli.STYLE_BLUE;
import static org.projectnessie.nessie.cli.cli.BaseNessieCli.STYLE_FAINT;
import static org.projectnessie.nessie.cli.cli.BaseNessieCli.STYLE_YELLOW;

import jakarta.annotation.Nonnull;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.iceberg.exceptions.RESTException;
import org.apache.iceberg.rest.RESTCatalog;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.projectnessie.client.NessieClientBuilder;
import org.projectnessie.client.api.NessieApiV2;
import org.projectnessie.client.config.NessieClientConfigSources;
import org.projectnessie.model.NessieConfiguration;
import org.projectnessie.model.Reference;
import org.projectnessie.nessie.cli.cli.BaseNessieCli;
import org.projectnessie.nessie.cli.cmdspec.ConnectCommandSpec;
import org.projectnessie.nessie.cli.grammar.Node;
import org.projectnessie.nessie.cli.grammar.Token;

public class ConnectCommand extends NessieCommand {

  static final Pattern NESSIE_URI_PATTERN = Pattern.compile("^(http.*/)api/v[1-5]/?" + "$");

  public ConnectCommand() {}

  @Override
  public void execute(@Nonnull BaseNessieCli cli, ConnectCommandSpec spec) throws Exception {

    PrintWriter writer = cli.writer();

    String uri = spec.getUri();

    RESTCatalog icebergClient = null;
    NessieApiV2 api = null;
    boolean failure = true;
    String initialReference = spec.getInitialReference();
    Map connectOptions = spec.getParameters();
    Map icebergProperties = new HashMap<>(connectOptions);
    Map nessieOptions = new HashMap<>(connectOptions);

    try {
      writer.println(
          new AttributedString(format("Connecting to %s ...", uri), STYLE_FAINT)
              .toAnsi(cli.terminal()));
      writer.flush();

      String nessieUri = uri;
      String icebergUri = uri;

      // Check if 'uri' represents a Nessie API base URI, if so, derive the Iceberg REST base URI.
      Matcher uriMatcher = NESSIE_URI_PATTERN.matcher(uri);
      boolean isNessieURI = uriMatcher.matches();
      if (isNessieURI) {
        icebergUri = uriMatcher.group(1) + "iceberg/";
      }

      // Derive Iceberg options from Nessie connect options and vice versa.
      String icebergToken = connectOptions.get("token");
      String nessieToken = connectOptions.get(CONF_NESSIE_AUTH_TOKEN);
      checkArgument(
          icebergToken == null || nessieToken == null || icebergToken.equals(nessieToken),
          "Supplied both 'token' and '%s' options, but with different values. Remove one of those options or use the same value.",
          CONF_NESSIE_AUTH_TOKEN);
      String bearerToken = icebergToken != null ? icebergToken : nessieToken;
      String nessieAuthType = connectOptions.get(CONF_NESSIE_AUTH_TYPE);
      if (bearerToken != null) {
        if (nessieAuthType == null) {
          nessieOptions.put(CONF_NESSIE_AUTH_TYPE, "BEARER");
        } else {
          checkArgument(
              "BEARER".equals(nessieAuthType),
              "Must use Nessie auth type %s = BEARER when providing a bearer token",
              CONF_NESSIE_AUTH_TYPE);
        }

        // "Propagate" the bearer token to both Nessie + Iceberg clients.
        icebergProperties.put("token", bearerToken);
        nessieOptions.put(CONF_NESSIE_AUTH_TOKEN, bearerToken);
      }

      // Ask the user to enter the token, if BEARER auth is configured.
      if ("BEARER".equals(nessieAuthType) && bearerToken == null) {
        writer.println(
            new AttributedString(
                    "\nUsing BEARER authentication, enter token (not echoed) + press ENTER:",
                    STYLE_BLUE.bold())
                .toAnsi(cli.terminal()));
        writer.flush();
        bearerToken = new String(System.console().readPassword()).trim();

        String warehouse = connectOptions.get("warehouse");
        if (warehouse != null) {
          icebergProperties.put("warehouse", warehouse);
        }

        nessieOptions.put(CONF_NESSIE_AUTH_TOKEN, bearerToken);
        icebergProperties.put("token", bearerToken);
      }

      try {
        icebergProperties.put("uri", icebergUri);

        // Use the "initial reference" clause from the CONNECT TO statement as the 'prefix'.
        if (initialReference != null) {
          icebergProperties.put("prefix", URLEncoder.encode(initialReference, UTF_8));
        }

        // Collect stdout+stderr to prevent the annoying exception printed by
        // org.apache.iceberg.rest.ErrorHandlers.DefaultErrorHandler when the remote endpoint does
        // not exist -  for example when connecting to an older Nessie instance w/o Iceberg REST.
        ByteArrayOutputStream errorCollector = new ByteArrayOutputStream();
        try {
          PrintStream out = System.out;
          PrintStream err = System.err;
          try (PrintStream ps = new PrintStream(errorCollector)) {
            System.setOut(ps);
            System.setErr(ps);
            // Having the 'new RESTCatalog()' here prevents the 'WARNING:
            // PropertyNamingStrategy.KebabCaseStrategy ...' message
            icebergClient = new RESTCatalog();
            icebergClient.initialize("iceberg", icebergProperties);
          } finally {
            System.setOut(out);
            System.setErr(err);
          }
        } catch (RESTException e) {
          throw e;
        } catch (Exception e) {
          writer.print(errorCollector);
          writer.flush();
          throw e;
        }

        Map properties = icebergClient.properties();
        if (Boolean.parseBoolean(properties.get("nessie.is-nessie-catalog"))) {
          nessieUri = properties.get("nessie.core-base-uri") + "v2/";
          if (initialReference == null) {
            initialReference = properties.get("nessie.default-branch.name");
          }
        }
        // Note: at one point we might extend the Nessie-CLI to work against any Iceberg-REST
        // service, but not yet. Contributions are welcome.

        writer.printf("Successfully connected to Iceberg REST at %s%n", icebergUri);
        writer.println(
            new AttributedString(
                    format("Connecting to Nessie REST at %s ...", nessieUri), STYLE_FAINT)
                .toAnsi(cli.terminal()));
        writer.flush();
      } catch (RESTException e) {
        writer.println(
            new AttributedString(
                    format("No Iceberg REST endpoint at %s ...", icebergUri), STYLE_YELLOW)
                .toAnsi(cli.terminal()));
        writer.flush();

        if (icebergClient != null) {
          icebergClient.close();
          icebergClient = null;
        }
      }

      NessieConfiguration config;

      CompletableFuture cancel = new CompletableFuture<>();
      Terminal terminal = cli.terminal();
      Terminal.SignalHandler sigIntHandler =
          terminal.handle(Terminal.Signal.INT, sig -> cancel.completeAsync(() -> null));
      try {
        api =
            NessieClientBuilder.createClientBuilderFromSystemSettings(
                    NessieClientConfigSources.mapConfigSource(nessieOptions))
                .withUri(nessieUri)
                .withCancellationFuture(cancel)
                .build(NessieApiV2.class);

        config = api.getConfig();
      } catch (Exception e) {
        if (api != null) {
          api.close();
        }

        if (hasCauseMatching(
            e,
            t ->
                t instanceof CancellationException
                    || t instanceof InterruptedException
                    || t instanceof TimeoutException)) {
          writer.println(
              new AttributedString("Connection request aborted or timed out.", STYLE_YELLOW)
                  .toAnsi(cli.terminal()));
          writer.println();
          writer.flush();

          return;
        }
        throw e;
      } finally {
        if (sigIntHandler != null) {
          terminal.handle(Terminal.Signal.INT, sigIntHandler);
        }
      }

      writer.printf(
          "Successfully connected to Nessie REST at %s - Nessie API version %d, spec version %s%n",
          nessieUri, config.getActualApiVersion(), config.getSpecVersion());

      cli.connected(api, icebergClient);
      failure = false;

      Reference ref;
      if (initialReference != null) {
        ref = api.getReference().refName(initialReference).get();
      } else {
        ref = api.getDefaultBranch();
      }

      cli.setCurrentReference(ref);
    } finally {
      if (failure) {
        try {
          if (api != null) {
            api.close();
          }
        } catch (Exception e) {
          // ignore
        }
        try {
          if (icebergClient != null) {
            icebergClient.close();
          }
        } catch (Exception e) {
          // ignore
        }
      }
    }
  }

  static boolean hasCauseMatching(Throwable t, Predicate test) {
    for (; t != null; t = t.getCause()) {
      if (test.test(t)) {
        return true;
      }
    }
    return false;
  }

  public String name() {
    return Token.TokenType.CONNECT.name();
  }

  public String description() {
    return "Connect to a Nessie repository.";
  }

  @Override
  public List> matchesNodeTypes() {
    return List.of(List.of(Token.TokenType.CONNECT));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy