Giter Club home page Giter Club logo

porter-helm3's Introduction

Helm3 Mixin for Porter

This is a Helm3 mixin for Porter. It executes the appropriate helm command based on which action it is included within: install, upgrade, or delete.

Install or Upgrade

Currently we only support the installation via --feed-url. Please make sure to install the mixin as follow:

porter mixin install helm3 --version v1.0.1 --feed-url https://mchorfa.github.io/porter-helm3/atom.xml

Mixin Configuration

Helm client version configuration. You can define others minors and patch versions up and down

- helm3:
    clientVersion: v3.8.2

Repositories

- helm3:
    repositories:
      stable:
        url: "https://charts.helm.sh/stable"

Mixin Syntax

Install

install:
  - helm3:
      description: "Description of the command"
      name: RELEASE_NAME
      chart: STABLE_CHART_NAME
      version: CHART_VERSION
      namespace: NAMESPACE
      devel: BOOL
      wait: BOOL # default true
      noHooks: BOOL # disable pre/post upgrade hooks (default false)
      skipCrds: BOOL # if set, no CRDs will be installed (default false)
      timeout:  DURATION # time to wait for any individual Kubernetes operation
      atomic: BOOL # if set to false, the install process will not roll back changes made in case the install fails (default true)
      debug: BOOL # enable verbose output (default false)
      set:
        VAR1: VALUE1
        VAR2: VALUE2
      values: # Array of paths to: Set/Override multiple values and multi-lines values
        - PATH_TO_THE_VALUES_FILE_1
        - PATH_TO_THE_VALUES_FILE_2
        - PATH_TO_THE_VALUES_FILE_3

Upgrade

upgrade:
  - helm3:
      description: "Description of the command"
      name: RELEASE_NAME
      chart: STABLE_CHART_NAME
      version: CHART_VERSION
      namespace: NAMESPACE
      resetValues: BOOL
      reuseValues: BOOL
      wait: BOOL # default true
      noHooks: BOOL # disable pre/post upgrade hooks (default false)
      skipCrds: BOOL # if set, no CRDs will be installed (default false)
      timeout:  DURATION # time to wait for any individual Kubernetes operation
      atomic: BOOL # if set to false, the upgrade process will not roll back changes made in case the upgrade fails (default true)
      debug: BOOL # enable verbose output (default false)
      set:
        VAR1: VALUE1
        VAR2: VALUE2
      values: # Array of paths to: Set/Override multiple values and multi-line values
        - PATH_TO_THE_VALUES_FILE_1
        - PATH_TO_THE_VALUES_FILE_2
        - PATH_TO_THE_VALUES_FILE_3

Uninstall

uninstall:
  - helm3:
      description: "Description of command"
      namespace: NAMESPACE
      releases:
        - RELEASE_NAME1
        - RELEASE_NAME2
      wait: BOOL # default false, if set It will wait for as long as --timeout
      noHooks: BOOL # prevent hooks from running during uninstallation
      timeout:  DURATION # time to wait for any individual Kubernetes operation
      debug: BOOL # enable verbose output (default false)

Outputs

The mixin supports saving secrets from Kubernetes as outputs.

outputs:
  - name: NAME
    secret: SECRET_NAME
    key: SECRET_KEY

The mixin also supports extracting resource metadata from Kubernetes as outputs.

outputs:
  - name: NAME
    resourceType: RESOURCE_TYPE
    resourceName: RESOURCE_TYPE_NAME
    namespace: NAMESPACE
    jsonPath: JSON_PATH_DEFINITION

Examples

Install

install:
  - helm3:
      description: "Install MySQL"
      name: mydb
      chart: stable/mysql
      version: 0.10.2
      namespace: mydb
      skipCrds: true
      set:
        mysqlDatabase: wordpress
        mysqlUser: wordpress
      values:
        - "./manifests/values_1.yaml"
        - "./manifests/values_2.yaml"
        - "./manifests/values_3.yaml"
      outputs:
        - name: mysql-root-password
          secret: mydb-mysql
          key: mysql-root-password
        - name: mysql-password
          secret: mydb-mysql
          key: mysql-password
        - name: mysql-cluster-ip
          resourceType: service
          resourceName: porter-ci-mysql-service
          namespace: "default"
          jsonPath: "{.spec.clusterIP}"

Upgrade

upgrade:
  - helm3:
      description: "Upgrade MySQL"
      name: porter-ci-mysql
      chart: stable/mysql
      version: 0.10.2
      wait: true
      resetValues: true
      reuseValues: false
      noHooks: true
      set:
        mysqlDatabase: mydb
        mysqlUser: myuser
        livenessProbe.initialDelaySeconds: 30
        persistence.enabled: true
      values:
        - "./manifests/values_1.yaml"
        - "./manifests/values_2.yaml"
        - "./manifests/values_3.yaml"

Uninstall

uninstall:
  - helm3:
      description: "Uninstall MySQL"
      namespace: mydb
      releases:
        - mydb
      wait: true
      noHooks: true

Execute

login:
  - helm3:
      description: "Login to OCI registry"
      arguments:
        - registry
        - login
        - localhost:5000
        - "--insecure"
      flags:
        u: myuser
        p: mypass

porter-helm3's People

Contributors

carolynvs avatar jarnfast avatar mchorfa avatar mcmchorfa avatar thomasthep avatar vdice avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

porter-helm3's Issues

Helm3 in a minikube enviroment times out

The following timeout occurs:

 porter install --cred spike-helm3 
 
installing spike-helm3-mysql...
executing install action from spike-helm3-mysql (installation: spike-helm3-mysql)
Install MySQL
/usr/local/bin/helm3 helm3 install my-mysql stable/mysql --version 1.6.2 --replace --set mysqlDatabase=mydb --set mysqlUser=mysql-admin
WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: *******
WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: *******
Error: Kubernetes cluster unreachable: Get "https://192.168.49.2:8443/version?timeout=32s": dial tcp 192.168.49.2:8443: i/o timeout
Error: exit status 1
err: exit status 1
Error: mixin execution failed: exit status 1
Error: 2 errors occurred:
	* container exit code: 1, message: <nil>. fetching outputs failed: error copying outputs from container: Error: No such container:path: 1b7c38509f55fdb4e6c48269d66b363982c8dabe9a267e5a08278789e1c0fb99:/cnab/app/outputs
	* required output mysql-password is missing and has no default

using porter.yaml with these contents:

# -----------------------------------------------------------------------------
# Spike: minikube
# Based on https://github.com/MChorfa/porter-helm3/blob/master/example/porter.yaml
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------
# Metadata
# https://porter.sh/author-bundles/#bundle-metadata
# -----------------------------------------------------------------------------

description: Spike to try out porter with MySql
name: spike-helm3-mysql
version: 0.1.0
registry: senzing

# -----------------------------------------------------------------------------
# Mixins
# https://porter.sh/author-bundles/#mixins
# https://porter.sh/mixins/
# -----------------------------------------------------------------------------

mixins:
- helm3:
    clientVersion: "v3.3.4"
    repositories:
      stable:
        url: "https://charts.helm.sh/stable"

# -----------------------------------------------------------------------------
# Credentials
# https://porter.sh/author-bundles/#credentials
# https://porter.sh/credentials/
# -----------------------------------------------------------------------------

credentials:
- default: ~/.kube/config
  description: File with embedded server and user certificates to connect to a kubernetes cluster. Helm should have already been init'd on this cluster.
  name: kubeconfig
  path: /root/.kube/config

# -----------------------------------------------------------------------------
# Parameters
# https://porter.sh/author-bundles/#parameters
# https://porter.sh/parameters/
# -----------------------------------------------------------------------------

parameters:
- name: database-name
  type: string
  default: mydb
- name: mysql-user
  type: string
  default: mysql-admin
- name: namespace
  type: string
  default: ''
- name: mysql-name
  type: string
  default: my-mysql

# -----------------------------------------------------------------------------
# Custom Actions
# https://porter.sh/author-bundles/#custom-actions
# -----------------------------------------------------------------------------

customActions:
  status:
    description: "Get the status of a helm3 release"
    modifies: false
    stateless: true

# -----------------------------------------------------------------------------
# Bundle Actions
# https://porter.sh/author-bundles/#bundle-actions
# -----------------------------------------------------------------------------

# -- install ------------------------------------------------------------------

install:
  - helm3:
      description: "Install MySQL"
      name: "{{ bundle.parameters.mysql-name }}"
      chart: stable/mysql
      version: 1.6.2
      namespace: "{{ bundle.parameters.namespace }}"
      replace: true
      set:
        mysqlDatabase: "{{ bundle.parameters.database-name}}"
        mysqlUser: "{{ bundle.parameters.mysql-user }}"
      outputs:
      - name: mysql-root-password
        secret: "{{ bundle.parameters.mysql-name }}"
        key: mysql-root-password
      - name: mysql-password
        secret: "{{ bundle.parameters.mysql-name }}"
        key: mysql-password

# -- status -------------------------------------------------------------------

status:
  - helm3:
      description: "MySQL Status"
      arguments:
        - status
        - "{{ bundle.parameters.mysql-name }}"
      flags:
        o: yaml

# -- upgrade ------------------------------------------------------------------

upgrade:
  - helm3:
      description: "Upgrade MySQL"
      name: "{{ bundle.parameters.mysql-name }}"
      namespace: "{{ bundle.parameters.namespace }}"
      chart: stable/mysql
      version: 1.6.2
      outputs:
      - name: mysql-root-password
        secret: "{{ bundle.parameters.mysql-name }}"
        key: mysql-root-password
      - name: mysql-password
        secret: "{{ bundle.parameters.mysql-name }}"
        key: mysql-password

# -- uninstall ----------------------------------------------------------------

uninstall:
  - helm3:
      description: "Uninstall MySQL"
      namespace: "{{ bundle.parameters.namespace }}"
      releases:
        - "{{ bundle.parameters.mysql-name }}"

# -----------------------------------------------------------------------------
# outputs
# https://porter.sh/author-bundles/#parameters
# https://porter.sh/wiring/#outputs
# -----------------------------------------------------------------------------

outputs:
  - name: mysql-password
    description: "The mysql database password"
    type: string
    applyTo:
      - install
      - upgrade
    sensitive: true

when running:

  1. Start cluster.
    Example:

    minikube start \
      --cpus 4 \
      --disk-size=50g  \
      --embed-certs \
      --memory 8192
  2. Build bundle.
    Example:

    porter build
  3. Create credentials
    Example:

    porter credentials generate spike-helm3
    1. Set:
      1. file path = /home/[username]/.kube/config
  4. Install bundle.
    Example:

    porter install --cred spike-helm3

Does this mixin support login to private registry?

Thank you for providing this mixin,
I am working on a project that need using private ACR (Azure).
It pulls helm chart from external ACR so it need to authenticate to ACR first.
But It seem this mixin doesn't support flag registry login? is it?

I am using exe mixin to run bash shell commands to login. But I think it'd better if helm3 mixin support that feature.

Mixin version missing with latest release

This mixin's version appears to be missing or ill-formatted.

$ porter version
porter v0.38.6 (43d077da)

$ porter mixin install helm3 --version v0.1.3 --url https://github.com/MChorfa/porter-helm3/releases/download
installed helm3 mixin  ()

Example with version:

$ porter mixin install kubernetes --feed-url https://cdn.porter.sh/mixins/atom.xml
installed kubernetes mixin v0.28.4 (f7dd720)

Allow bundle authors to ensure a namespace exists

Helm has re-added the --create-namespace flag, which checks if the namespace does not exist and creates it if needed. We should expose this flag as a field for the install and upgrade actions:

helm:
  chart: mychart
  namespace: "{{ installation.name }}"
  createNamespace: true

I suggest that createNamespace default to true, for the same reason that I proposed that upsert should be set by default (#30) or go even further with #17 for an idempotent install: mixins should default to idempotent commands for install/upgrade/uninstall. Re-running install or upgrade should not result in errors about resources already existing. We do not want to get people into situations where the bundle isn't runnable anymore and they have to fall back to using the tools manually to "fix" a stuck bundle.

Current workarounds for the lack of namespace support are:

  • Use the exec mixin to execute a helm install command with --create-namespace=true. Whenever a user has to use the exec mixin, it opens up security concerns for people evaluating the bundle, and it's an unnecessary hurdle for authors since supporting the extra flag is straightforward, so why make them switch to exec?
  • Use the kubernetes mixin and apply a namespace manifest. Again this is sub-par because it's a flag that helm supports, so having to switch to another tool (and increase the bundle size with another mixin) is unwarranted.

Suggestion: idempotent install

I would like to suggest a feature: "idempotent" installations. To illustrate what I mean: the company I work for is building a system that involves deploying a set of microservices to a kubernetes cluster, and we want to automate the deployment process. We don't want the deployment pipeline to need information about which services are already deployed and in which version; so it should just (re-)install every service whenever a new version of the system is to be deployed, even if not all of them have actually changed. This is accomplished by doing a 'porter install' using your mixin with the upsert flag (so it does not matter if the service is already installed or not).

However, this still has a small flaw: the mixin always installs the charts, leading to them getting a new revision number, even if nothing about the installation has changed (either the chart itself or its parameters). We would prefer if there was a way (i.e. via by an additional flag) to tell the mixin to compare the chart already installed on the cluster with the one about to be installed and not actually perform the installation if the 'new' installation is in fact identical to the current one.

I already implemented logic that accomplishes this (by essentially invoking a "helm get manifest" command to get the manifest of the installed release, then using the "helm template" command to render the manifest for the chart about to be installed, comparing the two and skipping the installation if they are identical) in a fork of your code for our internal use, but I've been wondering if you might be interested in including this in your project.

Support reading the kubeconfig file from any location

Porter is changing the user that the container runs as from root to a lower privileged user "nonroot". I quickly realized that this mixin has /root/.kube/config hard coded.

The kubernetes library we are using does support automatically detecting the KUBECONFIG environment variable, and looking up the kubeconfig file from $HOME/.kube/config. It would be great if this mixin supported that as well because otherwise it will immediately stop working with new releases of porter v1.

I have a patch that I'll submit for this shortly.

Document the values field

I noticed that the readme and porter's copy of the docs on porter.sh/mixins/helm3 do not include the values field, used to pass values files to helm. Let's make sure to document that so that people know that it is supported and how to specify multiple values.

Support the --timeout and --debug flags

Now that we specify --atomic by default, it may be a good idea to allow people to change the --timeout flag as well. When --atomic is set, helm waits for the deployment to be successful (until --timeout is reached) and then rollsback if needed. Some people may need to change --timeout either because they don't want to wait forever (the default timeout is like 5m) to realize the deployment failed, or because they want to wait even longer.

Along those same lines, when --atomic fails, it can be really hard to tell what was wrong! When --debug is set on the helm call, it will print out which deployment wasn't ready that caused helm to revert. Exposing --debug as well would help people figure out what's wrong.

Support Buildkit driver at build time

Porter v1 will support buildkit, we should design how we can mount secrets during porter build so that we can do things like pull a private chart and embed it into the bundle. That way the chart is available at runtime without having to specify credentials to use the chart at runtime.

Support --wait-for-jobs flag

The Helm 3 cli contains the --wait-for-jobs flag, which will cause helm to wait for all jobs to complete. There is currently no way to specify this flag in any way.

Can't escape these very common set lines

Either I want to know how to escape these using Helm3 and porter, or if it's an issue let's fix it!

the node selectors MUST be escaped in that fashion in order to work correctly on the command line. How should they be created in YAML using the Helm3 mixin in order to work correctly?

# Create namespace for the harbor nginx ingress controller 
kubectl create namespace harbor-ingress-system

# Add the nginx helm repo
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

# Install nginx ingress for Harbor
helm install harbor-nginx-ingress ingress-nginx/ingress-nginx \
    --namespace harbor-ingress-system \
    --set controller.ingressClass=harbor-nginx \
    --set controller.replicaCount=2 \
    --set controller.nodeSelector."beta\.kubernetes\.io/os"=linux \
    --set defaultBackend.nodeSelector."beta\.kubernetes\.io/os"=linux

Use upsert = true by default when installing

Someone should be able to repeat the install action of a bundle when it fails and have it properly handle when that step has been executed already.

For example, if I have a bundle with 2 steps: first it runs helm install and then it does something else, and the second step fails, I'm going to repeat the install command to get the second step to pass. If upsert isn't specified, then the bundle will fail because the release already exists.

Another scenario is a bundle author is iterating on their bundle and adding new stuff to it, and then re-running it. They should be able to repeat install over and over, and have it apply the newest changes to the bundle, without complaining that the release already exists.

By default, mixins should handle situations like this gracefully. One way to do that is to have upsert = true set. Another is more complicated, but I think #17 suggests checking if the release exists and is the same. I think that scenario requires a lot more work to make sure that nothing has changed (such as the chart is the same but a value is different). So this would be a good start.

can't build aarch64 due to missing OpenAPIv2

git clone
make build

github.com/MChorfa/porter-helm3/pkg/helm3 imports
        k8s.io/client-go/kubernetes imports
        k8s.io/client-go/discovery imports
        github.com/googleapis/gnostic/OpenAPIv2: module github.com/googleapis/gnostic@latest found (v0.5.3), but does not contain package github.com/googleapis/gnostic/OpenAPIv2
make: *** [Makefile:46: generate] Error 1

Either I'd like to build it, or you can cross-build and push a release for ARM64?

pinebook pro
Manjaro ARM
go version go1.15.2 linux/arm64

Uninstall doesn't handle already uninstalled releases gracefully

What did you do

  • Tried to uninstall a bundle
  • A step after the helm uninstall failed
  • Re-ran the uninstall

What did you expect to happen

  • The helm uninstall step ignoring the already uninstalled release

What did actually happen

  • The helm uninstall step failed

Terminal output from 2. uninstall

Uninstall helm release foo
/usr/local/bin/helm3 helm3 uninstall foo --namespace bar
Error: uninstall: Release not loaded: foo: release: not found
err: 1 error occurred:
        * exit status 1

Error: 1 error occurred:
        * exit status 1

Error: mixin execution failed: exit status 1
Error: 1 error occurred:
        * container exit code: 1, message: <nil>

Cause of problem

The error string from helm does not match the expected pattern defined in

if strings.Contains(output.String(), fmt.Sprintf(`release: %q not found`, release)) {

Which version of mixin

v0.1.9

Which version of helm

helm-v3.3.4-linux-amd64

Which license is the repository under?

I noticed that this repository has two licenses listed which I think was an accident from how the repo was created in GitHub. @MChorfa Did you intend to license this under the "unlicense" instead of the MIT license?

outputs fail because kubectl isn't installed

The mixin uses the kubectl binary to collect outputs (not secrets but when it collects arbitrary data from deployed services). If you only have the helm3 mixin installed, this fails because the mixin doesn't install kubectl.

The output collection function should either be updated to use the k8s sdk to retrieve outputs, or the mixin should install kubectl.

Mixin errors out when Porter attempts to get schema

It looks like this mixin is erroring out when Porter attempts to get its schema; because of this, the mixin isn't added to the global Porter schema and thus VSCode/auto-complete thinks usage of helm3 is invalid (because it isn't in the mixin list in Porter's schema).

(At least, I think that's roughly the problem ๐Ÿ˜„)

$ porter mixin install helm3 --feed-url https://mchorfa.github.io/porter-helm3/atom.xml
installed helm3 mixin v0.1.15-1-g502102c (502102c)

# This emulates what Porter will run to get the mixin schema
$ ~/.porter/mixins/helm3/helm3 schema
Error: stat /Users/vdice/go/src/github.com/MChorfa/porter-helm3/schema/schema.json: no such file or directory
err: stat /Users/vdice/go/src/github.com/MChorfa/porter-helm3/schema/schema.json: no such file or directory

However, note that when building this mixin locally and installing, no error occurs:

 $ make build install
GO111MODULE=on go mod tidy
GO111MODULE=on go generate ./...
mkdir -p bin/mixins/helm3
GO111MODULE=on go build -ldflags '-w -X github.com/MChorfa/porter-helm3/pkg.Version=v0.1.15-2-g4f18e33 -X github.com/MChorfa/porter-helm3/pkg.Commit=4f18e33' -o bin/mixins/helm3/helm3 ./cmd/helm3
mkdir -p bin/mixins/helm3
GOARCH=amd64 GOOS=linux GO111MODULE=on go build -ldflags '-w -X github.com/MChorfa/porter-helm3/pkg.Version=v0.1.15-2-g4f18e33 -X github.com/MChorfa/porter-helm3/pkg.Commit=4f18e33' -o bin/mixins/helm3/helm3-runtime ./cmd/helm3
cd pkg/helm3 && packr2 clean
# @porter mixin uninstall helm3
mkdir -p /Users/vdice/.porter/mixins/helm3/runtimes
install bin/mixins/helm3/helm3 /Users/vdice/.porter/mixins/helm3/helm3
install bin/mixins/helm3/helm3-runtime /Users/vdice/.porter/mixins/helm3/runtimes/helm3-runtime
# @porter mixin list

$ ~/.porter/mixins/helm3/helm3 schema
{
  "$schema":"http://json-schema.org/draft-07/schema#",
  "definitions":{
    "installStep":{
      "type":"object",
      "properties":{
        "helm3":{
...

--create-namespace flag is always set

The --create-namespace flag is always set, even when the namespace exists. The default behaviour for helm in that case is to try creating the namespace. However in some environments, users do not have the rights to create namespaces, resulting in an error:

Error: INSTALLATION FAILED: namespaces is forbidden: User "myuser" cannot create resource "namespaces" in API group "" at the cluster scope

I suggest adding the option to set it or not, potentially depending on a bundle parameter, in order to leave the choice to the end user.

UpSert flag is broken

The upsert flag is broken because it is mapped to the wrong yaml tag.

UpSert bool `yaml:"devel`

I suggest that install and upgrade should unconditionally run helm upgrade --install instead of having it be a flag. We should make the same change to the helm2 mixin as well! ๐Ÿ˜Š

One of the rules of a mixin is that it should allow the user to wrap imperative CLI commands (like helm install) in a more desired state or idemponent (retryable) fashion. Due to the nature of bundles, we have to design for every bundle action to be rerun. This is why for example the helm2 mixin gracefully handles errors when trying to delete a release that is already deleted.

I would be happy to submit a PR for this is you agree.

Cannot set helm values for keys containing '.'

In version 0.1.9 the following hack was introduced:

for _, k := range setKeys {
// Hack unitl helm introduce `--set-literal` for complex keys
// see https://github.com/helm/helm/issues/4030
// TODO : Fix this later upon `--set-literal` introduction
forcePointEscaping := strings.Replace(k, ".", "\\.", -1)
cmd.Args = append(cmd.Args, "--set", fmt.Sprintf("%s=%s", forcePointEscaping, step.Set[k]))

This appears to escape any . found in the keys for helm causing helm not to be able to override the wanted values.

From porter.yaml:

 - helm3:
      description: "Install Harbor"
      name: harbor
      chart: ./charts/harbor.tgz
      namespace: harbor
      replace: true
      wait: true
      set:
        database.external.host: "{{ bundle.parameters.database_host }}"
       <snipped>
      values:
        - valuefiles/harbor.yaml

Example from 0.1.8:

/cnab/app/mixins/helm3/helm3-runtime install --debug
/usr/local/bin/helm3 helm3 install harbor ./charts/harbor.tgz --namespace harbor --replace --wait \
  --values valuefiles/harbor.yaml --set database.external.host=host.docker.internal \
  <snipped>

Example from 0.1.9+:

/cnab/app/mixins/helm3/helm3-runtime install --debug
/usr/local/bin/helm3 helm3 install harbor ./charts/harbor.tgz --namespace harbor --replace --wait \
  --values valuefiles/harbor.yaml --set database\.external\.host=host.docker.internal \
  <snipped>

Expected outcome is that the rendered charts use host.docker.internal but actual behavior is that they're rendered using the default value defined in the chart's values.yaml.

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.