GithubHelp home page GithubHelp logo

csweichel / werft Goto Github PK

View Code? Open in Web Editor NEW
186.0 7.0 39.0 5.07 MB

Just Kubernetes Native CI

Home Page: https://werft.dev

License: MIT License

Go 47.82% HTML 0.28% TypeScript 15.92% JavaScript 33.97% CSS 1.68% Shell 0.12% Dockerfile 0.03% Mustache 0.17%
continuous-integration kubernetes

werft's Introduction

Gitpod Ready-to-Code

Werft

Werft is a Kubernetes-native CI system. It knows no pipelines, just jobs and each job is a Kubernetes pod. What you do in that pod is up to you. We do not impose a "declarative pipeline syntax" or some groovy scripting language. Instead, Werft jobs have run Node, Golang or bash scripts in production environments.



Installation

The easiest way to install Werft is using its Helm chart. Clone this repo, cd into helm/ and install using

helm dep update
helm upgrade --install werft .

Git-hoster integration

Werft integrates with Git hosting platforms using its plugin system. Currently, werft ships with support for GitHub only (plugins/github-repo and plugins/github-trigger).

To add support for other Git hoster, the github-repo plugin is a good starting point.

GitHub

To use werft with GitHub you'll need a GitHub app. To create the app, please follow the steps here.

When creating the app, please use following values:

Parameter Value Description
User authorization callback URL https://your-werft-installation.com/plugins/github-integration The /plugins/github-integration path is important, the domain should match your installation's config.baseURL
Webhook URL https://your-werft-installation.com/plugins/github-integration The /plugins/github-integration path is important, the domain should match your installation's config.baseURL
Permissions Contents: Read-Only
Commit Status: Read & Write
Issues: Read & Write
Pull Requests: Read & Write
Events Meta
Push
Issue Comments

Configuration

The following table lists the (incomplete set of) configurable parameters of the Werft chart and their default values. The helm chart's values.yaml is the reference for chart's configuration surface.

Parameter Description Default
repositories.github.webhookSecret Webhook Secret of your GitHub application. See GitHub Setup my-webhook-secret
repositories.github.privateKeyPath Path to the private key for your GitHub application. See GitHub setup secrets/github-app.com
repositories.github.appID AppID of your GitHub application. See GitHub setup secrets/github-app.com
repositories.github.installationID InstallationID of your GitHub application. Have a look at the Advanced page of your GitHub app to find thi s ID. secrets/github-app.com
config.baseURL URL of your Werft installatin https://demo.werft.dev
config.timeouts.preperation Time a job can take to initialize 10m
config.timeouts.total Total time a job can take 60m
config.gcOlderThan Garbage Collect logs and job metadata for jobs older than the configured value null
image.repository Image repository csweichel/werft
image.tag Image tag latest
image.pullPolicy Image pull policy Always
replicaCount Number of cert-manager replicas 1
rbac.create If true, create and use RBAC resources true
resources CPU/memory resource requests/limits
nodeSelector Node labels for pod assignment {}
affinity Node affinity for pod assignment {}

Specify each parameter using the --set key=value[,key=value] argument to helm install.

Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example,

$ helm install --name my-release -f values.yaml .

Tip: You can use the default values.yaml

OAuth

Werft does not support OAuth by itself. However, using OAuth Proxy that's easy enough to add.

Setting up jobs

Wert jobs are files in your repository where one file represents one job. A Werft job file mainly consists of the PodSpec that will be run. Werft will add a /workspace mount to your pod where you'll find the checked out repository the job is running on.

For example:

pod:
  containers:
  - name: hello-world
    image: alpine:latest
    workingDir: /workspace
    imagePullPolicy: IfNotPresent
    command:
    - sh 
    - -c
    - |
      echo Hello World
      ls

This job would print Hello World and list all files in the root of the repository.

Checkout werft's own build job for a more complete example.

Tip: You can use the werft CLI to create a new job using werft init job

GitHub events

Werft starts jobs based on GitHub push events if the repository contains a .werft/config.yaml file, e.g.

defaultJob: ".werft/build-job.yaml"
rules:
- path: ".werft/deploy.yaml"
  matchesAll:
  - or: ["repo.ref ~= refs/tags/"]
  - or: ["trigger !== deleted"]

The example above starts .werft/deploy.yaml for all tags. For everything else it will start .werft/build-job.yaml.

Log Cutting

Werft extracts structure from the log output its jobs produce. We call this process log cutting, because Werft understands logs as a bunch of streams/slices which have to be demultiplexed.

The default cutter in Werft expects the following syntax:

Code Command Description
[someID|PHASE] Some description here Enter new phase Enters into a new phase identified by someID and described by Some description here. All output in this phase that does not explicitely name a slice will use someID as slice.
[someID] Arbitrary output Log to a slice Logs Arbitrary output and marks it as part of the someID slice.
[someID|DONE] Finish a slice Marks the someID slice as done. No more output is expected from this slice in this phase.
[someID|FAIL] Reason Fail a slice Marks the someID slice as failed becuase of Reason. No more output is expected from this slice in this phase. Failing a slice does not automatically fail the job.
[type|RESULT] content Publish a result Publishes content as result of type type

Tip: You can produce this kind of log output using the Werft CLI: werft log

Authentication and Policies

Werft supports authentication and API-based policies on its gRPC interface. This allows for great flexibility and control over the actions users can perform.

Authentication

Authentication is performed using credential helper and auth plugins. The latter are plugins which can interpret tokens send as part of the gRPC metadata, e.g. by the CLI. See plugins/github-auth for an example auth plugin, and testdata/in-gitpod-config.yaml for an example setup.

Policies

Werft integrates the Open Policy Agent to afford flexible control over which actions are allowed via the API. Once enabled, all incoming gRPC calls are subject to the policy. Note: this does not affect the web UI or other plugins (e.g. the GitHub integration).

To enable API policies, set

service:
  apiPolicy:
    enabled: true
    paths: 
      - testdata/policy/api.rego

where paths is a list of files or directories which contain the policies.

For each API request, werft will provide the following input to evaluate the policy:

{
    "method": "/v1.WerftService/StartGitHubJob",
    "metadata": {
        ":authority": [
            "localhost:7777"
        ],
        "content-type": [
            "application/grpc"
        ],
        "user-agent": [
            "grpc-go/1.36.1"
        ],
        "x-auth-token": [
            "some-value"
        ]
    },
    "message": {
        "metadata": {
            "owner": "Christian Weichel",
            "repository": {
                "host": "github.com",
                "owner": "csweichel",
                "repo": "werft",
                "ref": "refs/heads/csweichel/support-token-based-access-116",
                "revision": "d2c02a67c6e13fc5c3b3f5afb3ae60a66add5caa"
            },
            "trigger": 1
        }
    },
    "auth": {
        "known": true,
        "username": "csweichel",
        "metadata": {
            "name": "Christian Weichel",
            "two-factor-authentication": "true"
        },
        "emails": [
            "[email protected]"
        ]
    }
}

The auth section is present only when an authentication plugin is configured, a token was sent, and the token/user is known to the auth plugins.

You can find an example policy in testdata/policy/api.rego.

Command Line Interface

Werft sports a powerful CLI which can be used to create, list, start and listen to jobs.

Installation

The Werft CLI is available on the GitHub release page, or using this one-liner:

curl -L werft.dev/get-cli.sh | sh

Usage

werft is a very simple GitHub triggered and Kubernetes powered CI system

Usage:
  werft [command]

Available Commands:
  help        Help about any command
  init        Initializes configuration for werft
  job         Interacts with currently running or previously run jobs
  log         Prints log-cuttable content
  run         Starts the execution of a job
  version     Prints the version of this binary

Flags:
  -h, --help          help for werft
      --host string   werft host to talk to (defaults to WERFT_HOST env var) (default "localhost:7777")
      --verbose       en/disable verbose logging

Use "werft [command] --help" for more information about a command.

Credential Helper

The werft CLI can send authentication tokens to werft, which are intepreted by the auth plugins for use with OPA policies. A credential helper is a program which prints a token on stdout and exits with code 0. Any other exit code will result in an error. The token is opaque to werft and interpreted by auth plugins (see Authentication and Policies).

Werft's CLI will pass context as a single line JSON via stdin to the credential helper. See testdata/credential-helper.sh for an example.

To enable this feature, set the WERFT_CREDENTIAL_HELPER env var to the command you would like to execute.

Annotations

Annotations are used by your werft job to make runtime decesions. Werft supports passing annotation in three ways:

  1. From PR description

You can add annotations in the following form to your Pull request description and werft will pick them up

/werft someAnnotation
/werft someAnnotation=foobar
- [x] /werft someAnnotation
- [x] /werft someAnnotation=foobar
  1. From Git commit

Werft supports same format as above to pass annotations via commit message. Werft will use the top most commit only.

  1. From CLI
werft run github -a someAnnotation=foobar

Attribution

Logo based on Shipyard Vectors by Vecteezy

Thank You

Thank you to our contributors:

werft's People

Contributors

adrienthebo avatar arthursens avatar corneliusludmann avatar csweichel avatar dependabot[bot] avatar easycz avatar filiptronicek avatar geropl avatar gtsiolis avatar iqqbot avatar jankeromnes avatar jankoehnlein avatar liam-j-bennett avatar vulkoingim avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

werft's Issues

turn text into links

when it makes sense, github repos, users, branches, maybe search for them or navigate to github

phase and success cells could take a user to a job or its result

Uncaught (in promise) TypeError: can't access property 0, this.state.error.metadata.headersMap['grpc-message'] is undefined

Describe the bug

Sometimes, loading a Werft job shows just a blank page.

Looking into the console, I see a bunch of:

Uncaught (in promise) TypeError: can't access property 0, this.state.error.metadata.headersMap['grpc-message'] is undefined

Reloading once typically helps.

To Reproduce

Steps to reproduce the behavior:

  1. Load a Werft job page
  2. Sometimes it's blank

Expected behavior

It should never be blank.

Screenshots

Additional context

Harmonise werft run and werft job

Werft run is the only CLI command which interacts with jobs (although they're not yet started) and does sit under werft job. We should make werft job run an alias for werft run.

"Finished" time is often in the future

Describe the bug
Screenshot 2020-02-21 at 08 24 14

To Reproduce
Steps to reproduce the behavior:

  1. Start a build and let it finish
  2. Sometimes(?) the "Finished" time is in the future

Expected behavior
I think the "Finished" time should be in the past if the build is finished.

Or, if it's a prediction, I think it should be called "Estimated to finish".

Export job run times via Prometheus

As operator for our dev-environment I want to have an overview of the job run times for some jobs. For example, if our build job suddenly takes much longer that may be something I want to react to.

A job-metrics plugin could provide this information on a Prometheus endpoint. This plugin would get a list of filter expressions and names. For each such entry we would produce a histogram of build times and a counter of successful builds, failed builds and total builds.

The configuration for this plugin could look like:

metrics:
# name will become part of the metric names which in this case would be
#   werft_job_metric_werft_build_duration_sec
#   werft_job_metric_werft_build_total
#   werft_job_metric_werft_build_success
#   werft_job_metric_werft_build_failure
- name: werft_build
  # filter are "AND'ed" filter expressions. A job has to match all of them to become part of the metric
  filter:
  - or: ["name ~== -build"]
  - or: ["repo.owner == csweichel"]
  - or: ["repo.repo == werft"]
  # histogram lists the time steps of duration the histogram
  histogram:
  - 0.01
  - 1
  - 10
  - 30
  - 60

[webui] Escaping issue with HTML-like strings

Describe the bug
The default value for annotations that are not set is <no value>. But the webui does not display this properly because it's parsed as HTML.

To Reproduce
Steps to reproduce the behavior:

  1. Start a job with an empty annotation
NAMESPACE="{{ .Annotations.namespace }}"
echo "ns: '$NAMESPACE'"
  1. See the log, observe how the ns: '' is printed

Expected behavior
See ns: '<no value>' printed in the ui.

Screenshots
image

Additional context
Browser: Firefox

Website needs a more obvious GitHub repo link

Is your feature request related to a problem? Please describe.
I'm always frustrated when I can't find the GitHub link on a website, even after scrolling up and down multiple times.

Then on a hunch I hovered on the "Learn More" button.

Describe the solution you'd like
Maybe adding a small but more explicit GitHub link, or icon, or ribbon somewhere in the page.

Describe alternatives you've considered
The "Learn More" button can stay as is, but on its own it doesn't help me to quickly find the GitHub repo.

Additional context

[web-ui] Job failure is not shown clearly

Describe the bug
When a job times out it's hard to miss this as the reason for job failure.
This is mainly a UX issue on the web-ui where we don't show "failure" clearly enough.

To Reproduce
Look at any timed out job and try to find the failure reason.

Expected behavior
A clearly positioned failure message.

Screenshots
It could look like this:
image

instead of

image

Simpler way to update the job list in on the "start job" page

Currently, when you you add/modify/delete jobs in git, those changes are not reflected on the "start job" webUI unless you run kubectl rollout restart statefulset werft.

It would be nice if weft would pull in those changes automatically of (if this is problematic due to performance) offer a "refresh" button on the "start job" webUI.

Limit or remove support for annotations in commit-messages

Is your feature request related to a problem? Please describe.
We had build failures on the main branch because annotations in commit messages parameterized the build with values that only made sense on a feature branch. Since breaking the build on main is problematic for the whole company, it would be nice to avoid this in the future.

Describe the solution you'd like
Remove support for annotations on commit messages and advice to use draft-PRs instead (and put the annotation in the description).

Code pointer:

func (s *GithubRepoServer) GetRemoteAnnotations(ctx context.Context, req *common.GetRemoteAnnotationsRequest) (resp *common.GetRemoteAnnotationsResponse, err error) {

Potential drawback: The first build of a feature branch will not have the annotations because it runs before the PR can be created.

Describe alternatives you've considered
Disable annotations on the main branch.

Additional context
@mads-hartmann brought up how annotation in commit messages can be problematic.
This issue captures a discussion I just hat with @csweichel.

Request: Please support branches with emojis in their name

Describe the bug

Trying to trigger Werft on a branch with an emoji in its name (e.g. refactor-๐ŸŒ) fails with:

๐Ÿ‘Ž cannot start job - please talk to whoever's in charge of your Werft installation

Example: gitpod-io/gitpod#3861 (comment)

To Reproduce
Steps to reproduce the behavior:

  1. Create a branch with an emoji in its name (e.g. refactor-๐ŸŒ)
  2. Run Werft on it
  3. It fails

Expected behavior
Werft should work & start a job.

Screenshots

Screenshot 2021-04-09 at 10 18 13

Additional context
(none)

[ui] Display results sorted in opposite order of completion

Is your feature request related to a problem? Please describe.
When browsing the result list it's the last item I'm interested in in 100% of the cases. If the list of results is long I have to scroll every time to find the most interesting result.

Describe the solution you'd like
Display the results in reverse order.

Describe alternatives you've considered

Additional context

Show when job is pulling the container image

When a job needs to pull the container image it looks like the job is just hanging there. I have to look at the Kubernetes events to understand what's going on.

Instead I'd like some feedback in the build log that we're now pulling the job container image, i.e. a simple log message.

Show job title in page title

When viewing the job details we should show the job name in the page title.
For example the page /jobs/foobar.1 should have werft - foobar.1 as title.

Wrong branch gets deployed

Describe the bug
When starting a Job from the web UI and branch which is not master, the revision from the master branch ist used.

To Reproduce

  1. start a job via web ui which is not named master
    image
  2. look at the Revision in the Job-Page-Head-Area and in the Prepare-Step and see that it's not the latest commit on your chosen branch.
  3. look at the branch "Ref" column in the Job-Listings-Page and see that it's "master" and not your chosen branch.

Expected behavior
I want my branch to be used.

Werft can produce invalid pod names when shortening

Describe the bug
When computing the pod name for a job werft accepts invalid pod names. For example: gitpod-wipe-devstaging-redirect-unauthenticated-.33

Expected behavior
Werft should produce a valid job name, i.e. one that follows this rule:
a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')

Make jobs time out

Faulty jobs run forever right now ... make them time out after configurable a time.

In this process we'll introduce

// AnnotationFailed explicitelly fails the job
AnnotationFailed = "werft.sh/failed"

We should re-use this field for stopping jobs to make sure they're marked as failed.

Favicon is still the react icon

The favicon should resemble the werft logo to some extend, i.e. have the hexagonal shape or be a filled in blob of the same shape.

werft run with unknown parameter should fail with an error

Describe the bug
If running werft with unknown parameter, werft should fail with an error instead of silently ignoring parameter.

To Reproduce
in gitpod repo, you can add this comment:
/werft run with-obsevability
notice there is a typo, but werft runs without saying anything.

Expected behavior
werft should fail to run if it detects unrecognized parameter

Screenshots
If applicable, add screenshots to help explain your problem.

Additional context
Add any other context about the problem here.

Restart a job

Enable users to restart a previously run job with the same (or possibly modified) metadata/annotations.

Search filter is not applied to new jobs

When you add a filter in the search bar of the jobs list the jobs are filtered accordingly. However, new jobs that come in via event stream are added regardless of the filter.

Steps to reproduce:

  • Add a filter to the search bar.
  • Create a job that does not meet the filter criteria.
  • โ†’ The job is added to the job list anyway.

Expected behavior:

  • The job is not added since it does not meet the filter criteria.

Add plugins

Plugins are processes that are started by werft. They're passed a single argument pointing to a socket where a gRPC server listens on. The gRPC interface differs based on the plugin type.

Different plugin types are:

  • integration plugins work with the https://github.com/32leaves/werft/tree/master/pkg/api/v1/werft.proto API. This kind of plugins adds new means of feedback or for triggering builds based on external events. The GitHub integration is a candidate.
  • gitauth plugins provide Git authentication similar to Git credential helpers. Their API does not exist yet.

Other types that might follow: "log store", "job store"

Plugins are configured in the main config file:

plugins:
  integration:
  - name: github
    command:
    - werft-github-integration
    - github-config.yaml
  - name: slack
    command: ["werft-slack-integration", "slack-config.yaml"]
  gitauth:
  - name: github
    command:
    - werft-github-gitauth
    - github-config.yaml

Werft ignores the "deleted" trigger

Describe the bug
A clear and concise description of what the bug is.
Jobs not getting triggered when branches are deleted.

We just introduced a new job that should be triggered whenever a branch is deleted(See gitpod-io/gitpod#8087). However, jobs are not being executed when we delete branches.

To Reproduce
Configure a Github repo to trigger jobs on branch deletions

Expected behavior
A clear and concise description of what you expected to happen.
Jobs being executed when branches are deleted

run: Allow git `tag` as `ref`

Is your feature request related to a problem? Please describe.
Every time I trigger werft run github org/repo:ref ... I expect it to take a tag as ref but it complains with:
Error: rpc error: code = Internal desc = GET https://api.github.com/repos/...: 404 Branch not found []

Describe the solution you'd like
Accept tags.

Describe alternatives you've considered
I can use the branch at whose HEAD the tag points to, but that's not always 100% the same.

Additional context

'werft run' vs. 'werft job run'

werft run and werft job run do the same, don't they?

If that's the case, choosing one and removing one would make it easier to google/GitHub-Search all werft run examples and documentation would become more homogeneous.

Start job from UI

Have a configurable list of repos from which we pull the werft config and offer the jobs.

Supporting libraries

Is your feature request related to a problem? Please describe.
Werft is a great tool to run super-specialized CI/CD pipelines. However, werft has its own singularities that are not widely spread, e.g. log slices and phases, and writing a job that follows werft standards can be quite difficult at the beginning.

Additionally, useful additions that are not related to the core logic of a job are often forgotten (e.g. measuring job/phase duration, setting up slack notifications, instrumenting tracing and/or pushgateway metrics) and that could be added to the supporting libraries.

Describe the solution you'd like
We could create libraries that specialize in giving support to developers who want to create/maintain werft jobs.
Starting with typescript and golang.

Describe alternatives you've considered
gitpod-io/gitpod has an okayish library that helps with logs under slices, but that's it. We'd need to replicate this code in other repositories if we'd like to use it.

Support pulling `.werft` from the default branch only

Is your feature request related to a problem? Please describe.
We want to be able to lock down CI build changes to prevent exfiltration of secrets via sneaky config changes.

Describe the solution you'd like
Make werft configurable so that only changes from main are pulled. If that's enabled, make sure we fail the job if there's a change on the branch.

Support token-based access on the gRPC interface

Is your feature request related to a problem? Please describe.
Today the gRPC interface is an all or nothing thing. Either you have access to it or not.

Describe the solution you'd like
Werft could provide some basic token-based protection on that interface.

Jobs are not triggered correctly in cron plugin

Describe the bug
We are running 4 jobs using the cron plugin @midnight and only the last job was running 3 times instead of on. After changing the scheduling to run the jobs with a 5 minutes gap, the last one was running at the time when the previous one should run and the at the time it should run.

image

https://werft.gitpod-dev.com/

More information can be found in the private issue https://github.com/gitpod-io/ops/issues/346

To Reproduce
Steps to reproduce the behavior:
Creating multiple jobs in the cron plugin should not work as expected.

Expected behavior
The jobs should run the time they are scheduled for.

[web-ui] Provide permalink to structured log output

Is your feature request related to a problem? Please describe.
When I want to send the link to a broken build to someone, I cannot link to to the exact part that broke.

Describe the solution you'd like
I'd like a link icon on an unfolded log output section which provides a permalink to that section.

Describe alternatives you've considered
The title of the section could be a link, but that would be confusing with the folding action.

[bug] Go-routine leak when subscribing to jobs

Werft seems to leak go-routines and memory over time.
After running werft for about 72h and running about 200 jobs in that time, we have 9883 go routines. Memory consumption has rissen to about 136 megabytes.

Do not associate jobs for branch names containing the same prefix

Describe the bug
Pushing two branches with the same prefix like gt/test and gt/test-two associates these two jobs.

To Reproduce
Steps to reproduce the behavior:

  1. Push a branch named gt/test
  2. Push a branch named gt/test-two
  3. Go to the job page of the first branch.
  4. See the bottom right notification.

Screenshots

Here's a notification at the bottom of the job page of the first branch where the second branch is associated.

Screenshot 2020-11-09 at 4 11 46 PM

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.