diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 50874b4e3638f..5a1b26e5d5ed3 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,5 +1,6 @@ name: Airbyte CI + on: schedule: - cron: '0 */6 * * *' @@ -7,7 +8,10 @@ on: jobs: launch_integration_tests: - runs-on: ubuntu-latest + strategy: + matrix: + runner: [ windows-latest, ubuntu-latest ] + runs-on: ${{ matrix.runner }} if: github.ref == 'refs/heads/master' steps: - name: Checkout Airbyte @@ -17,7 +21,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.SLASH_COMMAND_PAT }} build: - runs-on: ubuntu-latest + strategy: + matrix: + runner: [ windows-latest, ubuntu-latest ] + runs-on: ${{ matrix.runner }} steps: - name: Checkout Airbyte uses: actions/checkout@v2 diff --git a/airbyte-commons/build.gradle b/airbyte-commons/build.gradle index 7ce58844fea8c..ed66f4c283dba 100644 --- a/airbyte-commons/build.gradle +++ b/airbyte-commons/build.gradle @@ -3,5 +3,5 @@ plugins { } dependencies { - testImplementation 'org.apache.commons:commons-lang3:3.11' + implementation 'org.apache.commons:commons-lang3:3.11' } diff --git a/airbyte-commons/src/main/java/io/airbyte/commons/os/OsSupport.java b/airbyte-commons/src/main/java/io/airbyte/commons/os/OsSupport.java new file mode 100644 index 0000000000000..c8195a4441d44 --- /dev/null +++ b/airbyte-commons/src/main/java/io/airbyte/commons/os/OsSupport.java @@ -0,0 +1,66 @@ +/* + * MIT License + * + * Copyright (c) 2020 Airbyte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.airbyte.commons.os; + +import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.List; + +public class OsSupport { + + /** + * Formats the input command + * + * This helper is typically only necessary when calling external bash scripts. + * + * @param cmdParts + * @return + */ + public static String[] formatCmd(String... cmdParts) { + return formatCmd(new OsUtils(), cmdParts); + } + + public static String[] formatCmd(OsUtils osUtils, String... cmdParts) { + if (cmdParts.length == 0){ + return cmdParts; + } + + if (!cmdParts[0].endsWith(".sh")){ + // We need to apply windows formatting only for shell scripts since Windows doesn't support .sh files by default + // Other commands like `docker` should work fine + return cmdParts; + } + + List formattedParts = new ArrayList<>(); + if (osUtils.isWindows()) { + formattedParts.addAll(Lists.newArrayList("cmd", "/c")); + } + + formattedParts.addAll(List.of(cmdParts)); + + return formattedParts.toArray(new String[] {}); + } + +} diff --git a/airbyte-commons/src/main/java/io/airbyte/commons/os/OsUtils.java b/airbyte-commons/src/main/java/io/airbyte/commons/os/OsUtils.java new file mode 100644 index 0000000000000..d78e7976ed918 --- /dev/null +++ b/airbyte-commons/src/main/java/io/airbyte/commons/os/OsUtils.java @@ -0,0 +1,35 @@ +/* + * MIT License + * + * Copyright (c) 2020 Airbyte + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package io.airbyte.commons.os; + +import org.apache.commons.lang3.SystemUtils; + +public class OsUtils { + + public boolean isWindows() { + return SystemUtils.IS_OS_WINDOWS; + } + +} diff --git a/airbyte-commons/src/main/java/io/airbyte/commons/resources/MoreResources.java b/airbyte-commons/src/main/java/io/airbyte/commons/resources/MoreResources.java index 7203ae034dbde..5b6c99222a47c 100644 --- a/airbyte-commons/src/main/java/io/airbyte/commons/resources/MoreResources.java +++ b/airbyte-commons/src/main/java/io/airbyte/commons/resources/MoreResources.java @@ -75,12 +75,12 @@ public static Stream listResources(Class klass, String name) throws IOE @SuppressWarnings("UnstableApiUsage") public static void writeResource(String filename, String contents) { - final Path source = Paths.get(Resources.getResource("").getPath()); try { + final Path source = Paths.get(Resources.getResource("").toURI()); Files.deleteIfExists(source.resolve(filename)); Files.createFile(source.resolve(filename)); - IOs.writeFile(Path.of(Resources.getResource(filename).getPath()), contents); - } catch (IOException e) { + IOs.writeFile(Paths.get(Resources.getResource(filename).toURI()), contents); + } catch (Exception e) { throw new RuntimeException(e); } } diff --git a/airbyte-commons/src/test/java/io/airbyte/commons/json/JsonsTest.java b/airbyte-commons/src/test/java/io/airbyte/commons/json/JsonsTest.java index 90220ca3a6fe2..dc46262895cb3 100644 --- a/airbyte-commons/src/test/java/io/airbyte/commons/json/JsonsTest.java +++ b/airbyte-commons/src/test/java/io/airbyte/commons/json/JsonsTest.java @@ -234,8 +234,8 @@ void testKeys() { void testToPrettyString() { final JsonNode jsonNode = Jsons.jsonNode(ImmutableMap.of("test", "abc")); final String expectedOutput = "" - + "{\n" - + " \"test\": \"abc\"\n" + + "{" + System.lineSeparator() + + " \"test\": \"abc\"" + System.lineSeparator() + "}\n"; assertEquals(expectedOutput, Jsons.toPrettyString(jsonNode)); } diff --git a/airbyte-commons/src/test/java/io/airbyte/commons/resources/MoreResourcesTest.java b/airbyte-commons/src/test/java/io/airbyte/commons/resources/MoreResourcesTest.java index f13bd507dfe66..98ded26d67208 100644 --- a/airbyte-commons/src/test/java/io/airbyte/commons/resources/MoreResourcesTest.java +++ b/airbyte-commons/src/test/java/io/airbyte/commons/resources/MoreResourcesTest.java @@ -36,8 +36,8 @@ class MoreResourcesTest { @Test void testResourceRead() throws IOException { - assertEquals("content1\n", MoreResources.readResource("resource_test")); - assertEquals("content2\n", MoreResources.readResource("subdir/resource_test_sub")); + assertEquals("content1" + System.lineSeparator(), MoreResources.readResource("resource_test")); + assertEquals("content2" + System.lineSeparator(), MoreResources.readResource("subdir/resource_test_sub")); assertThrows(IllegalArgumentException.class, () -> MoreResources.readResource("invalid")); } diff --git a/airbyte-config/models/src/test/java/io/airbyte/config/EnvConfigsTest.java b/airbyte-config/models/src/test/java/io/airbyte/config/EnvConfigsTest.java index 2105385b29f90..e0d684e3616a2 100644 --- a/airbyte-config/models/src/test/java/io/airbyte/config/EnvConfigsTest.java +++ b/airbyte-config/models/src/test/java/io/airbyte/config/EnvConfigsTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.when; +import java.io.File; import java.nio.file.Paths; import java.util.function.Function; import org.junit.jupiter.api.Assertions; @@ -126,7 +127,8 @@ void testGetDatabaseUrl() { void testGetWorkspaceDockerMount() { when(function.apply(EnvConfigs.WORKSPACE_DOCKER_MOUNT)).thenReturn(null); when(function.apply(EnvConfigs.WORKSPACE_ROOT)).thenReturn("abc/def"); - Assertions.assertEquals("abc/def", config.getWorkspaceDockerMount()); + + Assertions.assertEquals("abc" + File.separator + "def", config.getWorkspaceDockerMount()); when(function.apply(EnvConfigs.WORKSPACE_DOCKER_MOUNT)).thenReturn("root"); when(function.apply(EnvConfigs.WORKSPACE_ROOT)).thenReturn(null); @@ -141,7 +143,7 @@ void testGetWorkspaceDockerMount() { void testGetLocalDockerMount() { when(function.apply(EnvConfigs.LOCAL_DOCKER_MOUNT)).thenReturn(null); when(function.apply(EnvConfigs.LOCAL_ROOT)).thenReturn("abc/def"); - Assertions.assertEquals("abc/def", config.getLocalDockerMount()); + Assertions.assertEquals("abc" + File.separator + "def", config.getLocalDockerMount()); when(function.apply(EnvConfigs.LOCAL_DOCKER_MOUNT)).thenReturn("root"); when(function.apply(EnvConfigs.LOCAL_ROOT)).thenReturn(null); diff --git a/airbyte-integrations/bases/airbyte-protocol/build.gradle b/airbyte-integrations/bases/airbyte-protocol/build.gradle index 983907a46c89d..2385be0a0bf72 100644 --- a/airbyte-integrations/bases/airbyte-protocol/build.gradle +++ b/airbyte-integrations/bases/airbyte-protocol/build.gradle @@ -9,7 +9,7 @@ airbytePython { task generateProtocolClassFilesWithoutLicense(type: Exec) { environment 'ROOT_DIR', rootDir.absolutePath - commandLine 'bin/generate-protocol-files.sh' + commandLine CrossPlatformSupport.formatCmd('bin/generate-protocol-files.sh') dependsOn ':tools:code-generator:airbyteDocker' } diff --git a/airbyte-scheduler/src/test/java/io/airbyte/scheduler/JobLogsTest.java b/airbyte-scheduler/src/test/java/io/airbyte/scheduler/JobLogsTest.java index 7517c80a6e494..aa5286e345805 100644 --- a/airbyte-scheduler/src/test/java/io/airbyte/scheduler/JobLogsTest.java +++ b/airbyte-scheduler/src/test/java/io/airbyte/scheduler/JobLogsTest.java @@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.nio.file.Path; import org.junit.jupiter.api.Test; class JobLogsTest { @@ -33,7 +34,7 @@ class JobLogsTest { @Test public void testGetLogDirectory() { final String actual = JobLogs.getLogDirectory("blah"); - final String expected = "logs/jobs/blah"; + final String expected = Path.of("logs/jobs/blah").toString(); assertEquals(expected, actual); } diff --git a/airbyte-server/src/main/java/io/airbyte/server/handlers/DebugInfoHandler.java b/airbyte-server/src/main/java/io/airbyte/server/handlers/DebugInfoHandler.java index 15de740e7f082..90095730ffc1c 100644 --- a/airbyte-server/src/main/java/io/airbyte/server/handlers/DebugInfoHandler.java +++ b/airbyte-server/src/main/java/io/airbyte/server/handlers/DebugInfoHandler.java @@ -29,6 +29,7 @@ import com.google.common.collect.Lists; import io.airbyte.api.model.DebugRead; import io.airbyte.commons.docker.DockerUtils; +import io.airbyte.commons.os.OsSupport; import io.airbyte.config.persistence.ConfigRepository; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -74,22 +75,24 @@ private static List> getRunningCoreImages() { "inspect", "--format='{{.Image}} {{.Config.Image}}'"); - inspectCommand.addAll(Lists.newArrayList(runningAirbyteContainers.split("\n"))); + inspectCommand.addAll(Lists.newArrayList(runningAirbyteContainers.split(System.lineSeparator()))); final String output = runAndGetOutput(inspectCommand).replaceAll("'", ""); - final List coreOutput = Lists.newArrayList(output.split("\n")); + final List coreOutput = Lists.newArrayList(output.split(System.lineSeparator())); - return coreOutput.stream().map(entry -> { - final String[] elements = entry.split(" "); - final String shortHash = getShortHash(elements[0]); - final String taggedImage = elements[1]; + return coreOutput.stream() + .filter(s -> !s.equals("")) + .map(entry -> { + final String[] elements = entry.split(""); + final String shortHash = getShortHash(elements[0]); + final String taggedImage = elements[1]; - final Map result = new HashMap<>(); - result.put("hash", shortHash); - result.put("image", taggedImage); - return result; - }).collect(toList()); + final Map result = new HashMap<>(); + result.put("hash", shortHash); + result.put("image", taggedImage); + return result; + }).collect(toList()); } catch (Exception e) { throw new RuntimeException(e); } @@ -122,7 +125,11 @@ private List> getIntegrationImages() { } protected static String runAndGetOutput(List cmd) throws IOException, InterruptedException { - final ProcessBuilder processBuilder = new ProcessBuilder(cmd); + return runAndGetOutput(cmd.toArray(new String[] {})); + } + + protected static String runAndGetOutput(String... cmd) throws IOException, InterruptedException { + final ProcessBuilder processBuilder = new ProcessBuilder(OsSupport.formatCmd(cmd)); final Process process = processBuilder.start(); process.waitFor(); diff --git a/airbyte-server/src/test/java/io/airbyte/server/handlers/DebugInfoHandlerTest.java b/airbyte-server/src/test/java/io/airbyte/server/handlers/DebugInfoHandlerTest.java index 591adcff4f556..89d97b58056b5 100644 --- a/airbyte-server/src/test/java/io/airbyte/server/handlers/DebugInfoHandlerTest.java +++ b/airbyte-server/src/test/java/io/airbyte/server/handlers/DebugInfoHandlerTest.java @@ -56,8 +56,8 @@ public void testNoFailures() throws ConfigNotFoundException, IOException, JsonVa @Test public void testRunAndGetOutput() throws IOException, InterruptedException { - final String expected = "hi"; - final String actual = DebugInfoHandler.runAndGetOutput(Lists.newArrayList("echo", "-n", "hi")); + final String expected = "hi" + System.lineSeparator(); + final String actual = DebugInfoHandler.runAndGetOutput("echo", "hi"); assertEquals(expected, actual); } diff --git a/airbyte-workers/src/main/java/io/airbyte/workers/process/DockerProcessBuilderFactory.java b/airbyte-workers/src/main/java/io/airbyte/workers/process/DockerProcessBuilderFactory.java index ea889898b8f4d..1988c529e43ac 100644 --- a/airbyte-workers/src/main/java/io/airbyte/workers/process/DockerProcessBuilderFactory.java +++ b/airbyte-workers/src/main/java/io/airbyte/workers/process/DockerProcessBuilderFactory.java @@ -29,6 +29,7 @@ import com.google.common.collect.Lists; import io.airbyte.commons.io.IOs; import io.airbyte.commons.io.LineGobbler; +import io.airbyte.commons.os.OsSupport; import io.airbyte.commons.resources.MoreResources; import io.airbyte.workers.WorkerException; import java.io.IOException; @@ -113,7 +114,7 @@ private Path rebasePath(final Path jobRoot) { @VisibleForTesting boolean checkImageExists(String imageName) { try { - final Process process = new ProcessBuilder(imageExistsScriptPath.toString(), imageName).start(); + final Process process = new ProcessBuilder(OsSupport.formatCmd(imageExistsScriptPath.toString(), imageName)).start(); LineGobbler.gobble(process.getErrorStream(), LOGGER::error); LineGobbler.gobble(process.getInputStream(), LOGGER::info); @@ -127,3 +128,5 @@ boolean checkImageExists(String imageName) { } } + + diff --git a/build.gradle b/build.gradle index 71239569bb4e5..33b33c69a23e5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,13 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +import java.util.stream.Stream +import java.util.stream.StreamSupport + plugins { id 'base' id 'pmd' id 'com.diffplug.spotless' version '5.7.0' + id "com.github.jlouns.cpe" version "0.5.0" } repositories { @@ -191,13 +197,9 @@ subprojects { } } -task composeBuild { - doFirst { - exec { - workingDir rootDir - commandLine 'docker-compose', '-f', 'docker-compose.build.yaml', 'build', '--parallel', '--quiet' - } - } +task composeBuild(type: CrossPlatformExec) { + workingDir rootDir + commandLine 'docker-compose', '-f', 'docker-compose.build.yaml', 'build', '--parallel', '--quiet' } build.dependsOn(composeBuild) diff --git a/buildSrc/src/main/groovy/CrossPlatformSupport.groovy b/buildSrc/src/main/groovy/CrossPlatformSupport.groovy new file mode 100644 index 0000000000000..af8ef24dde68c --- /dev/null +++ b/buildSrc/src/main/groovy/CrossPlatformSupport.groovy @@ -0,0 +1,19 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +class CrossPlatformSupport { + /** + * + * @param args The full command to be executed split into array. This is typically the input to `commandLine` in + * the exec task. + */ + static formatCmd(Object... cmdParts){ + List formattedParts = [] + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + formattedParts.addAll(['cmd', '/c']) + } + + formattedParts.addAll(cmdParts) + + return formattedParts.toArray() + } +} diff --git a/buildSrc/src/main/groovy/airbyte-docker.gradle b/buildSrc/src/main/groovy/airbyte-docker.gradle index 58865e44979c2..991a7c7ee82c9 100644 --- a/buildSrc/src/main/groovy/airbyte-docker.gradle +++ b/buildSrc/src/main/groovy/airbyte-docker.gradle @@ -41,7 +41,9 @@ abstract class AirbyteDockerTask extends DefaultTask { def tag = DockerHelpers.getDevTaggedImage(projectDir, dockerfileName) project.exec { - commandLine scriptPath, rootDir.absolutePath, projectDir.absolutePath, dockerfileName, tag, idFileOutput.absolutePath, followSymlinks + commandLine CrossPlatformSupport.formatCmd( + scriptPath, rootDir.absolutePath, projectDir.absolutePath, dockerfileName, tag, idFileOutput.absolutePath, followSymlinks + ) } } } @@ -87,7 +89,7 @@ class AirbyteDockerPlugin implements Plugin { def stdout = new ByteArrayOutputStream() project.exec { - commandLine "docker", "images", "--no-trunc", "-f", "dangling=false", "--format", "{{.ID}}", taggedImage + commandLine CrossPlatformSupport.formatCmd("docker", "images", "--no-trunc", "-f", "dangling=false", "--format", "{{.ID}}", taggedImage) standardOutput = stdout; } @@ -171,7 +173,7 @@ class AirbyteDockerPlugin implements Plugin { def imageToHash = [:] def stdout = new ByteArrayOutputStream() project.exec { - commandLine "docker", "images", "--no-trunc", "-f", "dangling=false", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}" + commandLine CrossPlatformSupport.formatCmd("docker", "images", "--no-trunc", "-f", "dangling=false", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}") standardOutput = stdout; } diff --git a/buildSrc/src/main/groovy/airbyte-source-test.gradle b/buildSrc/src/main/groovy/airbyte-source-test.gradle index 4db53186b9a4e..0cad897d62ac3 100644 --- a/buildSrc/src/main/groovy/airbyte-source-test.gradle +++ b/buildSrc/src/main/groovy/airbyte-source-test.gradle @@ -13,7 +13,8 @@ class AirbyteSourceTestPlugin implements Plugin { logger.info("imageName: ${imageName}") logger.info("pythonContainerName: ${pythonContainerName}") workingDir project.rootDir - commandLine 'docker', 'run', '--rm', '-i', + commandLine CrossPlatformSupport.formatCmd( + 'docker', 'run', '--rm', '-i', // so that it has access to docker '-v', "/var/run/docker.sock:/var/run/docker.sock", // when launching the container within a container, it mounts the directory from @@ -25,6 +26,7 @@ class AirbyteSourceTestPlugin implements Plugin { '--name', "std-test-${project.name}", 'airbyte/standard-source-test:dev', '--imageName', imageName, '--pythonContainerName', pythonContainerName + ) } } diff --git a/buildSrc/src/main/groovy/airbyte-standard-source-test-file.gradle b/buildSrc/src/main/groovy/airbyte-standard-source-test-file.gradle index 0f055c76451dd..15b0e0a52d4e5 100644 --- a/buildSrc/src/main/groovy/airbyte-standard-source-test-file.gradle +++ b/buildSrc/src/main/groovy/airbyte-standard-source-test-file.gradle @@ -1,5 +1,6 @@ import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.nativeplatform.toolchain.internal.gcc.CPCHCompiler abstract class AirbyteStandardSourceTestFileConfiguration { String configPath @@ -51,7 +52,7 @@ class AirbyteStandardSourceTestFilePlugin implements Plugin { args.add("${targetMountDirectory}/${config.statePath}") } - commandLine args + commandLine CrossPlatformSupport.formatCmd(args) } } diff --git a/docs/deploying-airbyte/on-your-workstation.md b/docs/deploying-airbyte/on-your-workstation.md index 1da5724e94311..4a168d4d72a13 100644 --- a/docs/deploying-airbyte/on-your-workstation.md +++ b/docs/deploying-airbyte/on-your-workstation.md @@ -4,11 +4,18 @@ These instructions have been tested on MacOS {% endhint %} -## Setup & launch Airbyte +## System requirements +The following software should be installed on your machine. +* Docker \(see [Docker](https://www.docker.com/products/docker-desktop). Note: There is a known issue with docker-compose 1.27.3. If you are using that version, please upgrade to 1.27.4. +* Java 14 +* Python 3.7.9 +* Node 14 -* Install Docker on your workstation \(see [instructions](https://www.docker.com/products/docker-desktop)\). Note: There is a known issue with docker-compose 1.27.3. If you are using that version, please upgrade to 1.27.4. -* Clone Airbyte's repository and run `docker compose` +#### A note on versions +These versions do not need to be your system defaults; they only need to be available. While Airbyte may function correctly with other versions of the requirements it is regularly tested with the versions listed above. +## Setup & launch Airbyte +Clone Airbyte's repository and run `docker compose ```bash # In your workstation terminal git clone https://github.com/airbytehq/airbyte.git @@ -16,10 +23,23 @@ cd airbyte docker-compose up ``` -* In your browser, just visit [http://localhost:8000](http://localhost:8000) -* Start moving some data! +After starting up, Airbyte will print a message to the terminal indicating it's ready: +``` + + ___ _ __ __ + / | (_)____/ /_ __ __/ /____ + / /| | / / ___/ __ \/ / / / __/ _ \ + / ___ |/ / / / /_/ / /_/ / /_/ __/ +/_/ |_/_/_/ /_.___/\__, /\__/\___/ + /____/ +-------------------------------------- + Now ready at http://localhost:8000/ +-------------------------------------- +``` + +In your browser, visit [http://localhost:8000](http://localhost:8000) and start moving some data! ## Troubleshooting -If you encounter any issues, just connect to our [Slack](https://slack.airbyte.io). Our community will help! +If you encounter any issues, just connect to our [Slack](https://slack.airbyte.io) or open an issue on our [Github repo](https://github.com/airbytehq/airbyte). Our community will help!