hisamounaのブログ

アウトプットを習慣化するためのブログ

yamlファイルを読み込む (Rust)

はじめに

Rustの勉強を始めました。

実践Rustプログラミング入門をひと通り読みました。わかりやすく解説されており、体系的に学べておすすめです。

アウトプットとして、手始めにyamlファイルの読み込んで出力される簡単なコードを書きました。

コード

実装

yamlのパースには、serde-yamlを使いました。

pub fn read_yaml(file_name: String) -> Result<Mapping> {
    let f = File::open(file_name)?;
    let reader = BufReader::new(f);
    let d :Mapping = serde_yaml::from_reader(reader)?;

    Ok(d)
}

苦労したこと

  • yamlのパース時にinvalid typeのエラーが発生

一番最初に書いたコード

let d = serde_yaml::from_reader(reader)?;

発生したエラー: Error: invalid type: map, expected unit

解決策: 型を明示的に定義する

let d :Mapping = serde_yaml::from_reader(reader)?;

普段はGoを書いているので、enumの扱いに不慣れです、、、

returnの型を定義

pub fn read_yaml(file_name: String) -> Result<Mapping> {

Rustを書いてみての感想 (Goと比較して)

  • Rsultと?を組み合わせることで、都度、if err != nil { return err} を書かなくていいのは便利

  • 別ファイルの関数を呼び出すときの手続きが必要。Goだと同一packageであれば必要なかったので新鮮でした。 1ファイル = 1moduleという考え方なんですね。

node上のpod一覧を表示する画面を作成する ~ (2) Reactで画面を作成 ~

前回の記事の続きです。 React触ってみました。

hisamouna.hatenablog.com

コード

axiosで前回作成したサーバに対してAPIリクエストし、node情報を取得する

定数(server)にaxios.createをセットすることで、複数axios.createを定義できるようにしました。

api/http-common.tsx

const server = axios.create({
  baseURL: "http://localhost:8090/v1",
  headers: {
    "Content-type": "application/json",
  }
})

const httpObject = {
  server
}

export default httpObject

apiリクエスト。Typescriptでレスポンスの型を定義できるのは良いですね。

api/api.tsx

export interface nodeDescribeInf {
  nodes: node[]
}

export interface node {
  name: string
  pods: pod[]
}

interface pod {
  namespace: string
  name: string
}

const nodeDescribe = () => {
  return http.server.get<nodeDescribeInf>("/node/describe");
}

レスポンス結果がうまくレンダリングされなかったので、修正

はじめのレンダリング時に、dataにデフォルトの値がなかったのが原因?

デフォルト値をセットすることで、axiosで非同期に取得されdataにセットされた値がレンダリングされる。

状態の管理のためにuseStateを使うのが基本のようです。

  var ndInf : nodeDescribeInf | undefined
  const [data, setData] = useState(ndInf)

  Api.nodeDescribe().then(
      ( response ) => {
        setData(response.data)
      }
   ).catch(
      err => console.log(`Error: ${err}`)
   )
.
.
.

    <div className="dashboard">
      {
        data &&
          data.nodes.map((v,i) =>
            <Node podNum={v.pods.length} nodeName={v.name} key={i}/>
          )
      }
    </div>

起動したところ、無限にAPIリクエストが実行されてしまう、、、

debugのためにconsole.logを仕込みました。

const nodeDescribe = () => {
  console.log("responder");
  return http.server.get<nodeDescribeInf>("/node/describe");
}

f:id:hisamouna:20210928132346p:plain

renderの中でuseStateしてstateを更新しているからのよう。

参考にしたブログ

useEffect(...,[])を使用して、最初のレンダリング時のみAPIリクエストを行うようにしました。

  useEffect(() => {
    Api.nodeDescribe().then(
      ( response ) => {
        setData(response.data)
      }
    ).catch(
      err => console.log(`Error: ${err}`)
    )
  },[])

期待通り画面が表示されました。

それぞれのnode上に起動しているpodsを表示しています。

f:id:hisamouna:20210928135922p:plain

node上のpod一覧を表示する画面を作成する ~ (1) Goでサーバを構築 ~

はじめに

フロントエンドの勉強をしてみたくなったので、Reactを触ってみました。

以前作ったGoのコードをベースにサーバーを構築し、ReactからAPIリクエストでnodeの情報を取得しブラウザ上で表示するようにしていきます。 hisamouna.hatenablog.com

本記事では、サーバの構築をしていきます。

コード

Protocol Buffersでリクエスト/レスポンスのインターフェースを定義

message NodesResponse {
  repeated Node nodes = 1;
}

message Node {
  string name = 1;
  repeated Pod pods = 2;
}

message Pod {
  string namespace = 1;
  string name = 2;
}

service NodeService {
  rpc DescribeNodes(google.protobuf.Empty) returns (NodesResponse) {
    option (google.api.http) = {
      get: "/v1/node/describe"
    };
  }
}

gRPCのコードを生成

└─> protoc -I ./pkg/server/node -I ${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis --go_out ./pkg/server/node --go_opt paths=source_relative --go-grpc_out ./pkg/server/node --go-grpc_opt paths=source_relative --grpc-gateway_out ./pkg/server/node --grpc-gateway_opt paths=source_relative ./pkg/server/node/node.proto

"google/protobuf/empty.proto";を使うときに注意すること

github.com

Unimplemented~をhandler用structに定義

type NodeHandler struct {
    node.UnimplementedNodeServiceServer
}

gRPCで定義されたインターフェースを満たすHandlerを作成

より良いエラーハンドリングを模索中、、、今回は、どこでエラーが発生したかを明確にしてみました。

func (handler *NodeHandler) DescribeNodes(ctx context.Context, in *emptypb.Empty) (*node.NodesResponse, error) {
    fmt.Println("handler.DescribeNodes")
    kc, err := kube.NewClient("kind_config")
    if err != nil {
        return nil, fmt.Errorf("DescribeNode(kueb.NewClient): %v", err)
    }
    nodes, err := kc.GetNodes()
    if err != nil {
        return nil, fmt.Errorf("GetNode(kueb.GetNodes): %v", err)
    }

    var response = &node.NodesResponse{}
    for _, nd := range nodes {
        dn, err := kc.DescribeNode(nd)
        if err != nil {
            return nil, fmt.Errorf("DescribeNode(kube.DescribeNode): %v", err)
        }
        var np = []*node.Pod{}
        for _, p := range dn.Pods {
            np = append(np, &node.Pod{
                Namespace: p.Namespace,
                Name:      p.Name,
            })
        }
        n := &node.Node{
            Name: dn.Name,
            Pods: np,
        }
        response.Nodes = append(response.Nodes, n)
    }
    return response, nil
}

ローカルで起動させて、動作確認

└─> go run cmd/server/main.go
2021/09/19 11:47:58 Serving gRPC on 0.0.0.0::8080

# 別ターミナルで
└─> go run cmd/gateway/main.go
2021/09/19 11:48:01 Serving gRPC-Gateway on http://0.0.0.0:8090

└─> curl http://localhost:8090/v1/node/describe | jq -r '.'
{
  "nodes": [
    {
      "name": "kinda-control-plane",
      "pods": [
        {
          "namespace": "kube-system",
          "name": "etcd-kinda-control-plane"
        },
        {
          "namespace": "kube-system",
          "name": "kindnet-c8fhf"
        },
.
.
.

client-goを使って、node上のpod一覧を表示する

環境

kindを使ってローカル上(Mac)にバージョン: v1.21.0のk8sクラスタを用意

クライアント作成

import (
    "context"
    "fmt"

    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/tools/clientcmd"
    "k8s.io/client-go/util/homedir"

    corev1 "k8s.io/api/core/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/fields"
)

type KubeClient struct {
    ClientSet *kubernetes.Clientset
}

func NewClient(fileName string) (*KubeClient, error) {
    kubeconfig := fmt.Sprintf("%s/.kube/%s", homedir.HomeDir(), fileName)

    config, err := clientcmd.BuildConfigFromFlags("", *&kubeconfig)
    if err != nil {
        return nil, err
    }
    client, err := kubernetes.NewForConfig(config)
    if err != nil {
        return nil, err
    }

    return &KubeClient{client}, err
}

node一覧を取得

KubernetesのlistNodesを参考

func (kc *KubeClient) GetNodes() ([]string, error) {
    nodes, err := kc.ClientSet.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
    if err != nil {
        return nil, err
    }
    var nodeNames []string
    for _, item := range nodes.Items {
        nodeNames = append(nodeNames, item.Name)
    }
    return nodeNames, nil
}

node上のpod一覧を取得

kubectlのDescribeを参考

func (kc *KubeClient) DescribeNode(name string) (interface{}, error) {
    fieldSelector, err := fields.ParseSelector("spec.nodeName=" + name + ",status.phase!=" + string(corev1.PodSucceeded) + ",status.phase!=" + string(corev1.PodFailed))
    if err != nil {
        return nil, err
    }
    nodeNonTerminatedPodsList, err := kc.ClientSet.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{FieldSelector: fieldSelector.String()})
    if err != nil {
        return nil, err
    }
    for _, pod := range nodeNonTerminatedPodsList.Items {
        fmt.Printf("%s\t%s\t\n", pod.Namespace, pod.Name)
    }
    return nil, nil
}

無事、一覧を取得できました。

テストコード

func TestGetNodes(t *testing.T) {
    kc, err := NewClient("kind_config")
    if err != nil {
        t.Fatalf("NewClient: %v", err)
    }

    nodes, err := kc.GetNodes()
    if err != nil {
        t.Fatalf("GetNodes: %v", err)
    }

    _, err = kc.DescribeNode(nodes[1])
    if err != nil {
        t.Fatalf("DescribeNode: %v", err)
    }
}
└─> go test -v pkg/kube/*.go
=== RUN   TestGetNodes
gatekeeper-system   gatekeeper-audit-d57fc8bf-flddz
gatekeeper-system   gatekeeper-controller-manager-6d68cfcdd8-hztlb
gatekeeper-system   gatekeeper-controller-manager-6d68cfcdd8-s5wp4
kube-system coredns-558bd4d5db-nb2rw
kube-system kindnet-bqnlc
kube-system kube-proxy-n69kv
--- PASS: TestGetNodes (0.03s)
PASS
ok      command-line-arguments  0.287s

kc.ClientSet.CoreV1().Pods("") だとすべてのnamespaceのpodを取得するので、特定のnamespaceのみの場合は kc.ClientSet.CoreV1().Pods("kube-system") と記載

SpreadSheetでチェックリストを使う (golang)

サンプルコード

コード

事前にやること

SpreadSheetのリンクを知っている人全員が編集できるようにする。

SpreadSheetID,SheetIDを取得

  • https://docs.google.com/spreadsheets/d/${SpreadSheetID}/edit#gid=${SheetID}

実行

           Range: &sheets.GridRange{
                StartRowIndex:    int64(1),
                EndRowIndex:      int64(3),
                StartColumnIndex: int64(1),
                EndColumnIndex:   int64(3),
                SheetId:          int64(sheetID),
            },

範囲をindexで指定 ((1,1) - (3,3) = B2 - C3)

Start~はinclusive, End~はexclusiveなので注意

空のシートに対して実行

SPREAD_SHEET_ID=${SpreadSheetID} SHEET_ID=${SheetID} go run main.go

f:id:hisamouna:20210911150746p:plain

デフォルトではチェックが付いていない状態(FALSE)でチェックボックスが作られる。

チェックを付ける

C2にチェックを付ける

   err = client.Update(fmt.Sprintf("%s!C2", os.Getenv("SHEET_NAME")), [][]interface{}{
        {
            true,
        },
    })

f:id:hisamouna:20210911152249p:plain

etcd (ローカルでPUT,GETする)

etcdをdokerで起動

Ref: Run etcd clusters inside containers

└─> export NODE1=192.168.1.21

└─> docker volume create --name etcd-data
etcd-data

└─> export DATA_DIR="etcd-data"

└─> REGISTRY=gcr.io/etcd-development/etcd

└─> docker run \
  -p 2379:2379 \
  -p 2380:2380 \
  --volume=${DATA_DIR}:/etcd-data \
  --name etcd ${REGISTRY}:latest \
  /usr/local/bin/etcd \
  --data-dir=/etcd-data --name node1 \
  --initial-advertise-peer-urls http://${NODE1}:2380 --listen-peer-urls http://0.0.0.0:2380 \
  --advertise-client-urls http://${NODE1}:2379 --listen-client-urls http://0.0.0.0:2379 \
  --initial-cluster node1=http://${NODE1}:2380

clientを実装

Ref: example code

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/coreos/etcd/clientv3"
    "google.golang.org/grpc/grpclog"
)

var (
    dialTimeout = 5 * time.Second

    endpoints = []string{"localhost:2379"}
)

func main() {
    clientv3.SetLogger(grpclog.NewLoggerV2(os.Stderr, os.Stderr, os.Stderr))

    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   endpoints,
        DialTimeout: dialTimeout,
    })
    if err != nil {
        log.Fatal(err)
    }
    defer cli.Close()

    _, err = cli.Put(context.TODO(), "foo", "bar")
    if err != nil {
        log.Fatal(err)
    }

    output, err := cli.Get(context.TODO(), "foo")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%+v\n", *output)
}

実行

go mod tidyが失敗する

  1. bboltのインストールパスが違う
└─> go mod tidy
.
.
.
github.com/coreos/etcd/clientv3 tested by
        github.com/coreos/etcd/clientv3.test imports
        github.com/coreos/etcd/auth imports
        github.com/coreos/etcd/mvcc/backend imports
        github.com/coreos/bbolt: github.com/coreos/bbolt@v1.3.6: parsing go.mod:
        module declares its path as: go.etcd.io/bbolt
                but was required as: github.com/coreos/bbolt

go.modでインストールパスをfix (issue)

// go.mod
replace github.com/coreos/bbolt v1.3.6 => go.etcd.io/bbolt v1.3.6

replace go.etcd.io/bbolt v1.3.6 => github.com/coreos/bbolt v1.3.6
  1. 最新のgrpcライブラリに google.golang.org/grpc/namingがない?
└─> go mod tidy
.
.
.
google.golang.org/grpc/naming: module google.golang.org/grpc@latest found (v1.40.0), but does not contain package google.golang.org/grpc/naming

ひとまず、grpcをダウングレード (issue)

// go.mod
replace google.golang.org/grpc v1.40.0 => google.golang.org/grpc v1.26.0

解決!

└─> go mod tidy
go: finding module for package github.com/coreos/etcd/clientv3
go: found github.com/coreos/etcd/clientv3 in github.com/coreos/etcd v3.3.25+incompatible
.
.
.

└─> go run main.go
INFO: 2021/09/05 14:00:31 parsed scheme: "endpoint"
INFO: 2021/09/05 14:00:31 ccResolverWrapper: sending new addresses to cc: [{localhost:2379  <nil> 0 <nil>}]
{Header:cluster_id:12743156124158006367 member_id:16536637930688536249 revision:4 raft_term:2  Kvs:[key:"foo" create_revision:2 mod_revision:4 version:3 value:"bar" ] More:false Count:1 XXX_NoUnkeyedLiteral:{} XXX_unrecognized:[] XXX_sizecache:0}

ArgoCDを構成するPod (メモ書き)

参考

Components

argocd-repo-server

ロール: Gitリポジトリをcloneする

cloneしたファイルをもとにconfig management(kustomize,helm)がmanifestsを生成する つまり、argocd-repo-server上でkustomize,helmコマンドが実行されている

cloneしたリポジトリは/tmp配下にstoreされる(TMPDIRで変更可能)

生成されたmanifestsはキャッシュしている(defaultで24時間) キャッシュはargocd-redisにstoreされる


argocd-application-controller

依存Components

  • argocd-repo-server : 生成されたmanifestを使うため

  • Kubernetes API server : 今のクラスタのstateを取得するため

kubectlコマンドを使ってクラスタへ変更を適用させている

3mおきにマニフェストの変更をチェックしている


argocd-server

ロール: WebUI,CLIやCI/CDで利用されるAPIを提供する


argocd-dex-server

configmapやsecretから生成した/tmp/dex.yamlを使ってdexコマンドを実行している

onelogin等を使うために必要


argocd-redis

redisはthrow-awayキャッシュとしてのみ使われる