αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžš Kubernetes αž“αŸ…αž€αŸ’αž“αž»αž„ Python αžŠαŸ„αž™αž‚αŸ’αž˜αžΆαž“αž€αŸ’αžšαž”αžαŸαžŽαŸ’αžŒ αž“αž·αž„ SDK

αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžš Kubernetes αž“αŸ…αž€αŸ’αž“αž»αž„ Python αžŠαŸ„αž™αž‚αŸ’αž˜αžΆαž“αž€αŸ’αžšαž”αžαŸαžŽαŸ’αžŒ αž“αž·αž„ SDK

αž”αž…αŸ’αž…αž»αž”αŸ’αž”αž“αŸ’αž“ Go αž˜αžΆαž“αžŸαž·αž‘αŸ’αž’αž·αž•αŸ’αžαžΆαž…αŸ‹αž˜αž»αžαž›αžΎαž—αžΆαžŸαžΆαžŸαžšαžŸαŸαžšαž€αž˜αŸ’αž˜αžœαž·αž’αžΈ αžŠαŸ‚αž›αž˜αž“αž»αžŸαŸ’αžŸαž‡αŸ’αžšαžΎαžŸαžšαžΎαžŸαžŠαžΎαž˜αŸ’αž”αžΈαžŸαžšαžŸαŸαžšαžŸαŸαž…αž€αŸ’αžαžΈαžαŸ’αž›αŸ‚αž„αž€αžΆαžšαžŽαŸαžŸαž˜αŸ’αžšαžΆαž”αŸ‹ Kubernetes αŸ” αž˜αžΆαž“αž αŸαžαž»αž•αž›αž‚αŸ„αž›αž”αŸ†αžŽαž„αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αžšαžΏαž„αž“αŸαŸ‡ αžŠαžΌαž…αž‡αžΆαŸ–

  1. αž˜αžΆαž“αž€αŸ’αžšαž”αžαŸαžŽαŸ’αžŒαžŠαŸαž˜αžΆαž“αž’αžΆαž“αž»αž—αžΆαž–αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž’αž—αž·αžœαžŒαŸ’αžαž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž“αŸ…αž€αŸ’αž“αž»αž„ Go - αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžš SDK.
  2. αž€αž˜αŸ’αž˜αžœαž·αž’αžΈαž•αŸ’αž›αžΆαžŸαŸ‹αž”αŸ’αžαžΌαžšαž αŸ’αž‚αŸαž˜αžŠαžΌαž…αž‡αžΆ Docker αž“αž·αž„ Kubernetes αžαŸ’αžšαžΌαžœαž”αžΆαž“αžŸαžšαžŸαŸαžšαž“αŸ…αž€αŸ’αž“αž»αž„ Go αŸ” αž€αžΆαžšαžŸαžšαžŸαŸαžšαž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαžšαž”αžŸαŸ‹αž’αŸ’αž“αž€αž“αŸ…αž€αŸ’αž“αž»αž„ Go αž˜αžΆαž“αž“αŸαž™αžαžΆαž“αž·αž™αžΆαž™αž—αžΆαžŸαžΆαžŠαžΌαž…αž‚αŸ’αž“αžΆαž‡αžΆαž˜αž½αž™αž“αžΉαž„αž”αŸ’αžšαž–αŸαž“αŸ’αž’αž’αŸαž€αžΌαŸ”
  3. αžŠαŸ†αžŽαžΎαžšαž€αžΆαžšαžαŸ’αž–αžŸαŸ‹αž“αŸƒαž€αž˜αŸ’αž˜αžœαž·αž’αžΈ Go αž“αž·αž„αž§αž”αž€αžšαžŽαŸαžŸαžΆαž˜αž‰αŸ’αž‰αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž’αŸ’αžœαžΎαž€αžΆαžšαž‡αžΆαž˜αž½αž™αž€αžΆαžšαžŸαŸ’αžšαž”αž‚αŸ’αž“αžΆαž€αŸ’αžšαŸ…αž”αŸ’αžšαž’αž”αŸ‹αŸ”

NBαŸ– αžŠαŸ„αž™αžœαž·αž’αžΈαž“αŸαŸ‡ αžšαž”αŸ€αž”αžŸαžšαžŸαŸαžšαžŸαŸαž…αž€αŸ’αžαžΈαžαŸ’αž›αŸ‚αž„αž€αžΆαžšαžŽαŸαž•αŸ’αž‘αžΆαž›αŸ‹αžαŸ’αž›αž½αž“αžšαž”αžŸαŸ‹αž’αŸ’αž“αž€αž“αŸ…αž€αŸ’αž“αž»αž„ Go αž™αžΎαž„ αž”αžΆαž“αž–αž·αž–αžŽαŸŒαž“αžΆαžšαž½αž…αž αžΎαž™ αž“αŸ…αž€αŸ’αž“αž»αž„αž€αžΆαžšαž”αž€αž”αŸ’αžšαŸ‚αžšαž”αžŸαŸ‹αž™αžΎαž„αžŠαŸ„αž™αž’αŸ’αž“αž€αž“αž·αž–αž“αŸ’αž’αž”αžšαž‘αŸαžŸαŸ”

αž”αŸ‰αž»αž“αŸ’αžαŸ‚αž…αž»αŸ‡αž™αŸ‰αžΆαž„αžŽαžΆαž”αžΎαž’αŸ’αž“αž€αžαŸ’αžšαžΌαžœαž”αžΆαž“αžšαžΆαžšαžΆαŸ†αž„αž–αžΈαž€αžΆαžšαžšαŸ€αž“ Go αžŠαŸ„αž™αžαŸ’αžœαŸ‡αž–αŸαž›αžœαŸαž›αžΆ αž¬αž“αž·αž™αžΆαž™αžŠαŸ„αž™αžŸαžΆαž˜αž‰αŸ’αž‰αžαžΆ αž€αžΆαžšαž›αžΎαž€αž‘αžΉαž€αž…αž·αžαŸ’αž? αž’αžαŸ’αžαž”αž‘αž•αŸ’αžαž›αŸ‹αž“αžΌαžœαž§αž‘αžΆαž αžšαžŽαŸαž˜αž½αž™αž’αŸ†αž–αžΈαžšαž”αŸ€αž”αžŠαŸ‚αž›αž’αŸ’αž“αž€αž’αžΆαž…αžŸαžšαžŸαŸαžšαžŸαŸαž…αž€αŸ’αžαžΈαžαŸ’αž›αŸ‚αž„αž€αžΆαžšαžŽαŸαžŠαŸαž›αŸ’αž’αžŠαŸ„αž™αž”αŸ’αžšαžΎαž—αžΆαžŸαžΆαžŠαŸαž–αŸαž‰αž“αž·αž™αž˜αž”αŸ†αž•αž»αžαž˜αž½αž™αžŠαŸ‚αž›αžŸαŸ’αž‘αžΎαžšαžαŸ‚αž‚αŸ’αžšαž”αŸ‹αžœαž·αžŸαŸ’αžœαž€αžš DevOps αžŠαžΉαž„ - αž–αžŸαŸ‹αžαŸ’αž›αžΆαž“αŸ‹.

αž‡αž½αž”αŸ– αž˜αŸ‰αžΆαžŸαŸŠαžΈαž“αžαžαž…αž˜αŸ’αž›αž„ - αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž…αž˜αŸ’αž›αž„!

αž‡αžΆαž§αž‘αžΆαž αžšαžŽαŸ αžŸαžΌαž˜αž–αž·αž…αžΆαžšαžŽαžΆαž”αž„αŸ’αž€αžΎαžαžŸαŸαž…αž€αŸ’αžαžΈαžαŸ’αž›αŸ‚αž„αž€αžΆαžšαžŽαŸαžŸαžΆαž˜αž‰αŸ’αž‰αž˜αž½αž™ αžŠαŸ‚αž›αžαŸ’αžšαžΌαžœαž”αžΆαž“αžšαž…αž“αžΆαž‘αžΎαž„αžŠαžΎαž˜αŸ’αž”αžΈαž…αž˜αŸ’αž›αž„ ConfigMap αž‘αžΆαŸ†αž„αž“αŸ…αž–αŸαž›αžŠαŸ‚αž› namespace αžαŸ’αž˜αžΈαž›αŸαž…αž‘αžΎαž„ αž¬αž“αŸ…αž–αŸαž›αžŠαŸ‚αž›αž’αžΆαžαž»αž˜αž½αž™αž€αŸ’αž“αž»αž„αž…αŸ†αžŽαŸ„αž˜αž–αžΈαžšαž•αŸ’αž›αžΆαžŸαŸ‹αž”αŸ’αžαžΌαžšαŸ– ConfigMap αž“αž·αž„ Secret αŸ” αžαžΆαž˜αž‘αžŸαŸ’αžŸαž“αŸˆαž‡αžΆαž€αŸ‹αžŸαŸ’αžαŸ‚αž„ αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž’αžΆαž…αž˜αžΆαž“αž”αŸ’αžšαž™αŸ„αž‡αž“αŸαžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž€αžΆαžšαž’αŸ’αžœαžΎαž”αž…αŸ’αž…αž»αž”αŸ’αž”αž“αŸ’αž“αž—αžΆαž–αž—αžΆαž‚αž…αŸ’αžšαžΎαž“αž“αŸƒαž€αžΆαžšαž€αŸ†αžŽαžαŸ‹αžšαž…αž“αžΆαžŸαž˜αŸ’αž–αŸαž“αŸ’αž’αž€αž˜αŸ’αž˜αžœαž·αž’αžΈ (αžŠαŸ„αž™αž€αžΆαžšαž’αžΆαž”αŸ‹αžŠαŸαž ConfigMap) αž¬αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž€αžΆαžšαž’αžΆαž”αŸ‹αžŠαŸαžαž‘αž·αž“αŸ’αž“αž“αŸαž™αžŸαž˜αŸ’αž„αžΆαžαŸ‹ - αž§αž‘αžΆαž αžšαžŽαŸ αž‚αŸ’αžšαžΆαž”αŸ‹αž…αž»αž…αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž’αŸ’αžœαžΎαž€αžΆαžšαž‡αžΆαž˜αž½αž™ Docker Registry (αž“αŸ…αž–αŸαž›αž”αž“αŸ’αžαŸ‚αž˜αž€αžΆαžšαžŸαž˜αŸ’αž„αžΆαžαŸ‹αž‘αŸ…αž€αŸ’αž“αž»αž„ namespace)αŸ”

αž αžΎαž™αžŠαžΌαž…αŸ’αž“αŸαŸ‡, αž’αŸ’αžœαžΈαžŠαŸ‚αž›αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž›αŸ’αž’αž‚αž½αžšαžαŸ‚αž˜αžΆαž“:

  1. αž’αž“αŸ’αžαžšαž€αž˜αŸ’αž˜αž‡αžΆαž˜αž½αž™αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαžαŸ’αžšαžΌαžœαž”αžΆαž“αž’αž“αž»αžœαžαŸ’αžαžŠαŸ„αž™αž”αŸ’αžšαžΎ αž“αž·αž™αž˜αž“αŸαž™αž’αž“αž’αžΆαž“αž•αŸ’αž‘αžΆαž›αŸ‹αžαŸ’αž›αž½αž“ (αžαž‘αŸ…αž“αŸαŸ‡αž αŸ…αžαžΆ CRD)αŸ”
  2. αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž’αžΆαž…αžαŸ’αžšαžΌαžœαž”αžΆαž“αž€αŸ†αžŽαžαŸ‹αžšαž…αž“αžΆαžŸαž˜αŸ’αž–αŸαž“αŸ’αž’αŸ” αžŠαžΎαž˜αŸ’αž”αžΈαž’αŸ’αžœαžΎαžŠαžΌαž…αŸ’αž“αŸαŸ‡ αž™αžΎαž„αž“αžΉαž„αž”αŸ’αžšαžΎαž‘αž„αŸ‹αž”αž“αŸ’αž‘αžΆαžαŸ‹αž–αžΆαž€αŸ’αž™αž”αž‰αŸ’αž‡αžΆ αž“αž·αž„αž’αžαŸαžšαž”αžšαž·αžŸαŸ’αžαžΆαž“αŸ”
  3. αž€αžΆαžšαž”αž„αŸ’αž€αžΎαžαž’αž»αž„ Docker αž“αž·αž„αž‚αŸ†αž“αžΌαžŸαžαžΆαž„ Helm αžαŸ’αžšαžΌαžœαž”αžΆαž“αžšαž…αž“αžΆαž‘αžΎαž„αžŠαžΎαž˜αŸ’αž”αžΈαž±αŸ’αž™αž’αŸ’αž“αž€αž”αŸ’αžšαžΎαž”αŸ’αžšαžΆαžŸαŸ‹αž’αžΆαž…αž„αžΆαž™αžŸαŸ’αžšαž½αž› (αžαžΆαž˜αž“αŸαž™αžαŸ’αžšαž„αŸ‹αžŠαŸ„αž™αž”αŸ’αžšαžΎαž–αžΆαž€αŸ’αž™αž”αž‰αŸ’αž‡αžΆαž˜αž½αž™) αžŠαŸ†αž‘αžΎαž„αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž‘αŸ…αž€αŸ’αž“αž»αž„αž…αž„αŸ’αž€αŸ„αž˜ Kubernetes αžšαž”αžŸαŸ‹αž–αž½αž€αž‚αŸαŸ”

ស៊ីឌីឌី

αžŠαžΎαž˜αŸ’αž”αžΈαž±αŸ’αž™αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαžŠαžΉαž„αž–αžΈαž’αž“αž’αžΆαž“αž’αŸ’αžœαžΈαžŠαŸ‚αž›αžαŸ’αžšαžΌαžœαžšαž€αž˜αžΎαž› αž“αž·αž„αž€αž“αŸ’αž›αŸ‚αž„αžŠαŸ‚αž›αžαŸ’αžšαžΌαžœαžšαž€αž˜αžΎαž› αž™αžΎαž„αžαŸ’αžšαžΌαžœαž€αŸ†αžŽαžαŸ‹αž…αŸ’αž”αžΆαž”αŸ‹αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž‚αžΆαžαŸ‹αŸ” αž…αŸ’αž”αžΆαž”αŸ‹αž“αžΈαž˜αž½αž™αŸ—αž“αžΉαž„αžαŸ’αžšαžΌαžœαž”αžΆαž“αžαŸ†αžŽαžΆαž„αž‡αžΆαžœαžαŸ’αžαž» CRD αžαŸ‚αž˜αž½αž™αŸ” តើ CRD αž“αŸαŸ‡αž‚αž½αžšαž˜αžΆαž“αžœαž·αžŸαŸαž™αž’αŸ’αžœαžΈαžαŸ’αž›αŸ‡?

  1. αž”αŸ’αžšαž—αŸαž‘αž’αž“αž’αžΆαž“αžŠαŸ‚αž›αž™αžΎαž„αž“αžΉαž„αžŸαŸ’αžœαŸ‚αž„αžšαž€ (ConfigMap ឬ Secret)αŸ”
  2. αž”αž‰αŸ’αž‡αžΈαžˆαŸ’αž˜αŸ„αŸ‡αž…αž“αŸ’αž›αŸ„αŸ‡αžŠαŸ‚αž›αž’αž“αž’αžΆαž“αž‚αž½αžšαžŸαŸ’αžαž·αžαž“αŸ…αŸ”
  3. αž€αž˜αŸ’αž˜αžœαž·αž’αžΈαž‡αŸ’αžšαžΎαžŸαžŠαŸ‚αž›αž™αžΎαž„αž“αžΉαž„αžŸαŸ’αžœαŸ‚αž„αžšαž€αž’αž“αž’αžΆαž“αž“αŸ…αž€αŸ’αž“αž»αž„ namespace αŸ”

αž…αžΌαžšαž–αžŽαŸŒαž“αžΆαž’αŸ†αž–αžΈ CRDαŸ–

apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
  name: copyrator.flant.com
spec:
  group: flant.com
  versions:
  - name: v1
    served: true
    storage: true
  scope: Namespaced
  names:
    plural: copyrators
    singular: copyrator
    kind: CopyratorRule
    shortNames:
    - copyr
  validation:
    openAPIV3Schema:
      type: object
      properties:
        ruleType:
          type: string
        namespaces:
          type: array
          items:
            type: string
        selector:
          type: string

αž αžΎαž™αž™αžΎαž„αž“αžΉαž„αž”αž„αŸ’αž€αžΎαžαžœαžΆαž—αŸ’αž›αžΆαž˜αŸ— αž…αŸ’αž”αžΆαž”αŸ‹αžŸαžΆαž˜αž‰αŸ’αž‰ - αžŠαžΎαž˜αŸ’αž”αžΈαžŸαŸ’αžœαŸ‚αž„αžšαž€αž€αŸ’αž“αž»αž„αž…αž“αŸ’αž›αŸ„αŸ‡αžˆαŸ’αž˜αŸ„αŸ‡αž‡αžΆαž˜αž½αž™αžˆαŸ’αž˜αŸ„αŸ‡ default ConfigMap αž‘αžΆαŸ†αž„αž’αžŸαŸ‹αžŠαŸ‚αž›αž˜αžΆαž“αžŸαŸ’αž›αžΆαž€αžŠαžΌαž…αž‡αžΆ copyrator: "true":

apiVersion: flant.com/v1
kind: CopyratorRule
metadata:
  name: main-rule
  labels:
    module: copyrator
ruleType: configmap
selector:
  copyrator: "true"
namespace: default

αžšαž½αž…αžšαžΆαž›αŸ‹αž αžΎαž™! αž₯αž‘αžΌαžœαž“αŸαŸ‡ αž™αžΎαž„αžαŸ’αžšαžΌαžœαž‘αž‘αž½αž›αž”αžΆαž“αž–αŸαžαŸŒαž˜αžΆαž“αž’αŸ†αž–αžΈαž…αŸ’αž”αžΆαž”αŸ‹αžšαž”αžŸαŸ‹αž™αžΎαž„αŸ” αž’αž“αž»αž‰αŸ’αž‰αžΆαžαž±αŸ’αž™αžαŸ’αž‰αž»αŸ†αž’αŸ’αžœαžΎαž€αžΆαžšαž€αž€αŸ‹αž‘αž»αž€αž—αŸ’αž›αžΆαž˜αŸ—αžαžΆαž™αžΎαž„αž“αžΉαž„αž˜αž·αž“αžŸαžšαžŸαŸαžšαžŸαŸ†αžŽαžΎαž‘αŸ…αž€αžΆαž“αŸ‹ Cluster API Server αžαŸ’αž›αž½αž“αž™αžΎαž„αž‘αŸαŸ” αžŠαžΎαž˜αŸ’αž”αžΈαž’αŸ’αžœαžΎαžŠαžΌαž…αŸ’αž“αŸαŸ‡ αž™αžΎαž„αž“αžΉαž„αž”αŸ’αžšαžΎαž”αžŽαŸ’αžŽαžΆαž›αŸαž™ Python αžŠαŸ‚αž›αžαŸ’αžšαŸ€αž˜αžšαž½αž…αž‡αžΆαžŸαŸ’αžšαŸαž… kubernetes-αž’αžαž·αžαž·αž‡αž“:

import kubernetes
from contextlib import suppress


CRD_GROUP = 'flant.com'
CRD_VERSION = 'v1'
CRD_PLURAL = 'copyrators'


def load_crd(namespace, name):
    client = kubernetes.client.ApiClient()
    custom_api = kubernetes.client.CustomObjectsApi(client)

    with suppress(kubernetes.client.api_client.ApiException):
        crd = custom_api.get_namespaced_custom_object(
            CRD_GROUP,
            CRD_VERSION,
            namespace,
            CRD_PLURAL,
            name,
        )
    return {x: crd[x] for x in ('ruleType', 'selector', 'namespace')}

αž‡αžΆαž›αž‘αŸ’αž’αž•αž›αž“αŸƒαž€αžΆαžšαžŠαŸ†αžŽαžΎαžšαž€αžΆαžšαž€αžΌαžŠαž“αŸαŸ‡ αž™αžΎαž„αž‘αž‘αž½αž›αž”αžΆαž“αžŠαžΌαž…αžαžΆαž„αž€αŸ’αžšαŸ„αž˜αŸ–

{'ruleType': 'configmap', 'selector': {'copyrator': 'true'}, 'namespace': ['default']}

αž’αžŸαŸ’αž…αžΆαžšαŸ’αž™αŸ– αž™αžΎαž„αž’αžΆαž…αž‘αž‘αž½αž›αž”αžΆαž“αž…αŸ’αž”αžΆαž”αŸ‹αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαŸ” αž αžΎαž™αžŸαŸ†αžαžΆαž“αŸ‹αž”αŸ†αž•αž»αž αž™αžΎαž„αž”αžΆαž“αž’αŸ’αžœαžΎαž’αŸ’αžœαžΈαžŠαŸ‚αž›αž αŸ…αžαžΆαžœαž·αž’αžΈ KubernetesαŸ”

αž’αžαŸαžšαž”αžšαž·αžŸαŸ’αžαžΆαž“ αž¬αž‘αž„αŸ‹αž‡αžΆαžαž·? αž™αžΎαž„αž™αž€αž‘αžΆαŸ†αž„αž’αžŸαŸ‹!

αž…αžΌαžšαž”αž“αŸ’αžαž‘αŸ…αž€αžΆαžšαž€αŸ†αžŽαžαŸ‹αžšαž…αž“αžΆαžŸαž˜αŸ’αž–αŸαž“αŸ’αž’αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαžŸαŸ†αžαžΆαž“αŸ‹αŸ” αž˜αžΆαž“αžœαž·αž’αžΈαžŸαžΆαžŸαŸ’αžšαŸ’αžαž‡αžΆαž˜αžΌαž›αžŠαŸ’αž‹αžΆαž“αž…αŸ†αž“αž½αž“αž–αžΈαžšαž€αŸ’αž“αž»αž„αž€αžΆαžšαž€αŸ†αžŽαžαŸ‹αžšαž…αž“αžΆαžŸαž˜αŸ’αž–αŸαž“αŸ’αž’αž€αž˜αŸ’αž˜αžœαž·αž’αžΈαŸ–

  1. αž”αŸ’αžšαžΎαž‡αž˜αŸ’αžšαžΎαžŸαž”αž“αŸ’αž‘αžΆαžαŸ‹αž–αžΆαž€αŸ’αž™αž”αž‰αŸ’αž‡αžΆ;
  2. αž”αŸ’αžšαžΎαž’αžαŸαžšαž”αžšαž·αžŸαŸ’αžαžΆαž“αŸ”

αž‡αž˜αŸ’αžšαžΎαžŸαž”αž“αŸ’αž‘αžΆαžαŸ‹αž–αžΆαž€αŸ’αž™αž”αž‰αŸ’αž‡αžΆαž’αž“αž»αž‰αŸ’αž‰αžΆαžαž±αŸ’αž™αž’αŸ’αž“αž€αž’αžΆαž“αž€αžΆαžšαž€αŸ†αžŽαžαŸ‹αž€αžΆαž“αŸ‹αžαŸ‚αž”αžαŸ‹αž”αŸ‚αž“ αžŠαŸ„αž™αž˜αžΆαž“αž€αžΆαžšαž‚αžΆαŸ†αž‘αŸ’αžšαž”αŸ’αžšαž—αŸαž‘αž‘αž·αž“αŸ’αž“αž“αŸαž™ αž“αž·αž„αžŸαž»αž–αž›αž—αžΆαž–αŸ” αž”αžŽαŸ’αžŽαžΆαž›αŸαž™αžŸαŸ’αžαž„αŸ‹αžŠαžΆαžšαžšαž”αžŸαŸ‹ Python αž˜αžΆαž“αž˜αŸ‰αžΌαžŒαž»αž›αž˜αž½αž™αŸ” argparserαžŠαŸ‚αž›αž™αžΎαž„αž“αžΉαž„αž”αŸ’αžšαžΎαŸ” αž–αŸαžαŸŒαž˜αžΆαž“αž›αž˜αŸ’αž’αž·αž αž“αž·αž„αž§αž‘αžΆαž αžšαžŽαŸαž“αŸƒαžŸαž˜αžαŸ’αžαž—αžΆαž–αžšαž”αžŸαŸ‹αžœαžΆαž˜αžΆαž“αž“αŸ…αž€αŸ’αž“αž»αž„ αž―αž€αžŸαžΆαžšαž•αŸ’αž›αžΌαžœαž€αžΆαžš.

αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž€αžšαžŽαžΈαžšαž”αžŸαŸ‹αž™αžΎαž„ αž“αŸαŸ‡αž‚αžΊαž‡αžΆαž’αŸ’αžœαžΈαžŠαŸ‚αž›αž§αž‘αžΆαž αžšαžŽαŸαž“αŸƒαž€αžΆαžšαžŠαŸ†αž‘αžΎαž„αž€αžΆαžšαž’αžΆαž“αž‘αž„αŸ‹αž”αž“αŸ’αž‘αžΆαžαŸ‹αž–αžΆαž€αŸ’αž™αž”αž‰αŸ’αž‡αžΆαž“αžΉαž„αž˜αžΎαž›αž‘αŸ…αžŠαžΌαž…αŸ–

   parser = ArgumentParser(
        description='Copyrator - copy operator.',
        prog='copyrator'
    )
    parser.add_argument(
        '--namespace',
        type=str,
        default=getenv('NAMESPACE', 'default'),
        help='Operator Namespace'
    )
    parser.add_argument(
        '--rule-name',
        type=str,
        default=getenv('RULE_NAME', 'main-rule'),
        help='CRD Name'
    )
    args = parser.parse_args()

αž˜αŸ’αž™αŸ‰αžΆαž„αžœαž·αž‰αž‘αŸ€αž αžŠαŸ„αž™αž”αŸ’αžšαžΎαž’αžαŸαžšαž”αžšαž·αžŸαŸ’αžαžΆαž“αž“αŸ…αž€αŸ’αž“αž»αž„ Kubernetes αž’αŸ’αž“αž€αž’αžΆαž…αž•αŸ’αž‘αŸαžšαž–αŸαžαŸŒαž˜αžΆαž“αžŸαŸαžœαžΆαž€αž˜αŸ’αž˜αž’αŸ†αž–αžΈαž•αžαž“αŸ…αž€αŸ’αž“αž»αž„αž’αž»αž„αž”αžΆαž“αž™αŸ‰αžΆαž„αž„αžΆαž™αžŸαŸ’αžšαž½αž›αŸ” αž§αž‘αžΆαž αžšαžŽαŸ αž™αžΎαž„β€‹αž’αžΆαž…β€‹αž‘αž‘αž½αž›β€‹αž”αžΆαž“β€‹αž–αŸαžαŸŒαž˜αžΆαž“β€‹αž’αŸ†αž–αžΈβ€‹αž‘αŸ†αž αŸ†β€‹αžˆαŸ’αž˜αŸ„αŸ‡β€‹αžŠαŸ‚αž›β€‹αž•αžβ€‹αž€αŸ†αž–αž»αž„β€‹αžŠαŸ†αžŽαžΎαžšαž€αžΆαžšβ€‹αž‡αžΆαž˜αž½αž™β€‹αž“αžΉαž„β€‹αž€αžΆαžšβ€‹αžŸαŸ’αžαžΆαž”αž“αžΆβ€‹αžŠαžΌαž…β€‹αžαžΆαž„β€‹αž€αŸ’αžšαŸ„αž˜αŸ–

env:
- name: NAMESPACE
  valueFrom:
     fieldRef:
         fieldPath: metadata.namespace 

αžαž€αŸ’αž€αžœαž·αž‡αŸ’αž‡αžΆαž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžš

αžŠαžΎαž˜αŸ’αž”αžΈαž™αž›αŸ‹αž–αžΈαžšαž”αŸ€αž”αž”αŸ†αž”αŸ‚αž€αžœαž·αž’αžΈαžŸαžΆαžŸαŸ’αžšαŸ’αžαžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž’αŸ’αžœαžΎαž€αžΆαžšαž‡αžΆαž˜αž½αž™ ConfigMap αž“αž·αž„ Secret αž™αžΎαž„αž“αžΉαž„αž”αŸ’αžšαžΎαž•αŸ‚αž“αž‘αžΈαž–αž·αžŸαŸαžŸαŸ” αž”αž“αŸ’αž‘αžΆαž”αŸ‹αž˜αž€αž™αžΎαž„αž’αžΆαž…αž™αž›αŸ‹αžαžΆαžαžΎαž™αžΎαž„αžαŸ’αžšαžΌαžœαž€αžΆαžšαžœαž·αž’αžΈαžŽαžΆαžŠαžΎαž˜αŸ’αž”αžΈαžαžΆαž˜αžŠαžΆαž“ αž“αž·αž„αž”αž„αŸ’αž€αžΎαžαžœαžαŸ’αžαž»αŸ–

LIST_TYPES_MAP = {
    'configmap': 'list_namespaced_config_map',
    'secret': 'list_namespaced_secret',
}

CREATE_TYPES_MAP = {
    'configmap': 'create_namespaced_config_map',
    'secret': 'create_namespaced_secret',
}

αž”αž“αŸ’αž‘αžΆαž”αŸ‹αž˜αž€ αž’αŸ’αž“αž€αžαŸ’αžšαžΌαžœαž‘αž‘αž½αž›αž–αŸ’αžšαžΉαžαŸ’αžαž·αž€αžΆαžšαžŽαŸαž–αžΈαž˜αŸ‰αžΆαžŸαŸŠαžΈαž“αž˜αŸ APIαŸ” αž…αžΌαžšαž™αžΎαž„αž’αž“αž»αžœαžαŸ’αžαžœαžΆαžŠαžΌαž…αžαžΆαž„αž€αŸ’αžšαŸ„αž˜αŸˆ

def handle(specs):
    kubernetes.config.load_incluster_config()
    v1 = kubernetes.client.CoreV1Api()

    # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΌΠ΅Ρ‚ΠΎΠ΄ для слСТСния Π·Π° ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π°ΠΌΠΈ
    method = getattr(v1, LIST_TYPES_MAP[specs['ruleType']])
    func = partial(method, specs['namespace'])

    w = kubernetes.watch.Watch()
    for event in w.stream(func, _request_timeout=60):
        handle_event(v1, specs, event)

αž”αž“αŸ’αž‘αžΆαž”αŸ‹αž–αžΈαž‘αž‘αž½αž›αž”αžΆαž“αž–αŸ’αžšαžΉαžαŸ’αžαž·αž€αžΆαžšαžŽαŸαž“αŸαŸ‡ αž™αžΎαž„αž”αž“αŸ’αžαž‘αŸ…αžαž€αŸ’αž€αžœαž·αž‡αŸ’αž‡αžΆαž…αž˜αŸ’αž”αž„αž“αŸƒαž€αžΆαžšαžŠαŸ†αžŽαžΎαžšαž€αžΆαžšαžœαžΆαŸ–

# Π’ΠΈΠΏΡ‹ событий, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±ΡƒΠ΄Π΅ΠΌ Ρ€Π΅Π°Π³ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ
ALLOWED_EVENT_TYPES = {'ADDED', 'UPDATED'}


def handle_event(v1, specs, event):
    if event['type'] not in ALLOWED_EVENT_TYPES:
        return

    object_ = event['object']
    labels = object_['metadata'].get('labels', {})

    # Π˜Ρ‰Π΅ΠΌ совпадСния ΠΏΠΎ selector'Ρƒ
    for key, value in specs['selector'].items():
        if labels.get(key) != value:
            return
    # ΠŸΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Π΅ namespace'Ρ‹
    namespaces = map(
        lambda x: x.metadata.name,
        filter(
            lambda x: x.status.phase == 'Active',
            v1.list_namespace().items
        )
    )
    for namespace in namespaces:
        # ΠžΡ‡ΠΈΡ‰Π°Π΅ΠΌ ΠΌΠ΅Ρ‚Π°Π΄Π°Π½Π½Ρ‹Π΅, устанавливаСм namespace
        object_['metadata'] = {
            'labels': object_['metadata']['labels'],
            'namespace': namespace,
            'name': object_['metadata']['name'],
        }
        # Π’Ρ‹Π·Ρ‹Π²Π°Π΅ΠΌ ΠΌΠ΅Ρ‚ΠΎΠ΄ создания/обновлСния ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π°
        methodcaller(
            CREATE_TYPES_MAP[specs['ruleType']],
            namespace,
            object_
        )(v1)

αžαž€αŸ’αž€αžœαž·αž‡αŸ’αž‡αžΆαžŸαŸ†αžαžΆαž“αŸ‹αž‚αžΊαžšαž½αž…αžšαžΆαž›αŸ‹αž αžΎαž™! αž₯αž‘αžΌαžœαž“αŸαŸ‡αž™αžΎαž„αžαŸ’αžšαžΌαžœαžαŸ’αž…αž”αŸ‹αž‘αžΆαŸ†αž„αž’αžŸαŸ‹αž“αŸαŸ‡αž‘αŸ…αž€αŸ’αž“αž»αž„αž€αž‰αŸ’αž…αž”αŸ‹ Python αž˜αž½αž™αŸ” αž™αžΎαž„αžšαŸ€αž”αž…αŸ†αž―αž€αžŸαžΆαžš setup.pyαžŸαžšαžŸαŸαžšαž–αŸαžαŸŒαž˜αžΆαž“αž˜αŸαžαžΆαž’αŸ†αž–αžΈαž‚αž˜αŸ’αžšαŸ„αž„αž“αŸ…αž‘αžΈαž“αŸ„αŸ‡αŸ–

from sys import version_info

from setuptools import find_packages, setup

if version_info[:2] < (3, 5):
    raise RuntimeError(
        'Unsupported python version %s.' % '.'.join(version_info)
    )


_NAME = 'copyrator'
setup(
    name=_NAME,
    version='0.0.1',
    packages=find_packages(),
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.5',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
    ],
    author='Flant',
    author_email='[email protected]',
    include_package_data=True,
    install_requires=[
        'kubernetes==9.0.0',
    ],
    entry_points={
        'console_scripts': [
            '{0} = {0}.cli:main'.format(_NAME),
        ]
    }
)

NBαŸ– αž˜αŸ‰αžΆαžŸαŸŠαžΈαž“αž—αŸ’αž‰αŸ€αžœ kubernetes αžŸαž˜αŸ’αžšαžΆαž”αŸ‹ Python αž˜αžΆαž“αž€αŸ†αžŽαŸ‚αž•αŸ’αž‘αžΆαž›αŸ‹αžαŸ’αž›αž½αž“αžšαž”αžŸαŸ‹αžœαžΆαŸ” αž–αŸαžαŸŒαž˜αžΆαž“αž”αž“αŸ’αžαŸ‚αž˜αž’αŸ†αž–αžΈαž—αžΆαž–αžαŸ’αžšαžΌαžœαž‚αŸ’αž“αžΆαžšαžœαžΆαž„αž€αŸ†αžŽαŸ‚αž˜αŸ‰αžΆαžŸαŸŠαžΈαž“αž—αŸ’αž‰αŸ€αžœ αž“αž·αž„αž€αŸ†αžŽαŸ‚ Kubernetes αž’αžΆαž…αžšαž€αž”αžΆαž“αž“αŸ…αž€αŸ’αž“αž»αž„ αž˜αŸ‰αžΆαž‘αŸ’αžšαžΈαžŸαž—αžΆαž–αž†αž”αž‚αŸ’αž“αžΆαŸ”.

αž₯αž‘αžΌαžœαž“αŸαŸ‡αž‚αž˜αŸ’αžšαŸ„αž„αžšαž”αžŸαŸ‹αž™αžΎαž„αž˜αžΎαž›αž‘αŸ…αžŠαžΌαž…αž“αŸαŸ‡αŸ–

copyrator
β”œβ”€β”€ copyrator
β”‚   β”œβ”€β”€ cli.py # Π›ΠΎΠ³ΠΈΠΊΠ° Ρ€Π°Π±ΠΎΡ‚Ρ‹ с ΠΊΠΎΠΌΠ°Π½Π΄Π½ΠΎΠΉ строкой
β”‚   β”œβ”€β”€ constant.py # ΠšΠΎΠ½ΡΡ‚Π°Π½Ρ‚Ρ‹, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΏΡ€ΠΈΠ²ΠΎΠ΄ΠΈΠ»ΠΈ Π²Ρ‹ΡˆΠ΅
β”‚   β”œβ”€β”€ load_crd.py # Π›ΠΎΠ³ΠΈΠΊΠ° Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ CRD
β”‚   └── operator.py # Основная Π»ΠΎΠ³ΠΈΠΊΠ° Ρ€Π°Π±ΠΎΡ‚Ρ‹ ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€Π°
└── setup.py # ΠžΡ„ΠΎΡ€ΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΏΠ°ΠΊΠ΅Ρ‚Π°

Docker αž“αž·αž„ Helm

Dockerfile αž“αžΉαž„αž˜αžΆαž“αž›αž€αŸ’αžαžŽαŸˆαžŸαžΆαž˜αž‰αŸ’αž‰αž˜αž·αž“αž‚αž½αžšαž±αŸ’αž™αž‡αžΏαŸ– αž™αž€αžšαžΌαž”αž—αžΆαž– python-alpine αž˜αžΌαž›αžŠαŸ’αž‹αžΆαž“ αž αžΎαž™αžŠαŸ†αž‘αžΎαž„αž€αž‰αŸ’αž…αž”αŸ‹αžšαž”αžŸαŸ‹αž™αžΎαž„αŸ” αžŸαžΌαž˜αž–αž“αŸ’αž™αžΆαžšαž–αŸαž›αž€αžΆαžšαž”αž„αŸ’αž€αžΎαž“αž”αŸ’αžšαžŸαž·αž‘αŸ’αž’αž—αžΆαž–αžšαž”αžŸαŸ‹αžœαžΆαžšαž αžΌαžαžŠαž›αŸ‹αž–αŸαž›αžœαŸαž›αžΆαž€αžΆαž“αŸ‹αžαŸ‚αž”αŸ’αžšαžŸαžΎαžšαŸ–

FROM python:3.7.3-alpine3.9

ADD . /app

RUN pip3 install /app

ENTRYPOINT ["copyrator"]

αž€αžΆαžšαžŠαžΆαž€αŸ‹αž–αž„αŸ’αžšαžΆαž™αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž€αŸαžŸαžΆαž˜αž‰αŸ’αž‰αžŽαžΆαžŸαŸ‹αžŠαŸ‚αžšαŸ–

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}
spec:
  selector:
    matchLabels:
      name: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        name: {{ .Chart.Name }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: privaterepo.yourcompany.com/copyrator:latest
        imagePullPolicy: Always
        args: ["--rule-type", "main-rule"]
        env:
        - name: NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
      serviceAccountName: {{ .Chart.Name }}-acc

αž‡αžΆαž…αž»αž„αž€αŸ’αžšαŸ„αž™ αž’αŸ’αž“αž€αžαŸ’αžšαžΌαžœαž”αž„αŸ’αž€αžΎαžαžαž½αž“αžΆαž‘αžΈαžŸαž˜αžŸαŸ’αžšαž”αž˜αž½αž™αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαžŠαŸ‚αž›αž˜αžΆαž“αžŸαž·αž‘αŸ’αž’αž·αž…αžΆαŸ†αž”αžΆαž…αŸ‹αŸ–

apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{ .Chart.Name }}-acc

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: {{ .Chart.Name }}
rules:
  - apiGroups: [""]
    resources: ["namespaces"]
    verbs: ["get", "watch", "list"]
  - apiGroups: [""]
    resources: ["secrets", "configmaps"]
    verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: {{ .Chart.Name }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: {{ .Chart.Name }}
subjects:
- kind: ServiceAccount
  name: {{ .Chart.Name }}

αž›αž‘αŸ’αž’αž•αž›

αž“αŸ„αŸ‡αž αžΎαž™αž‡αžΆαžšαž”αŸ€αž”αžŠαŸ‚αž›αžŠαŸ„αž™αž‚αŸ’αž˜αžΆαž“αž€αžΆαžšαž—αŸαž™αžαŸ’αž›αžΆαž… αž€αžΆαžšαžŸαŸ’αžαžΈαž”αž“αŸ’αž‘αŸ„αžŸ αž¬αžšαŸ€αž“ Go αž™αžΎαž„αž’αžΆαž…αž”αž„αŸ’αž€αžΎαžαž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž•αŸ’αž‘αžΆαž›αŸ‹αžαŸ’αž›αž½αž“αžšαž”αžŸαŸ‹αž™αžΎαž„αžŸαž˜αŸ’αžšαžΆαž”αŸ‹ Kubernetes αž“αŸ…αž€αŸ’αž“αž»αž„ Python αŸ” αž‡αžΆαž€αžΆαžšαž–αž·αžαžŽαžΆαžŸαŸ‹ αžœαžΆαž“αŸ…αžαŸ‚αž˜αžΆαž“αž€αž“αŸ’αž›αŸ‚αž„αžŠαžΎαž˜αŸ’αž”αžΈαžšαžΈαž€αž…αž˜αŸ’αžšαžΎαž“αŸ– αž“αŸ…αž–αŸαž›αž’αž“αžΆαž‚αž αžœαžΆαž“αžΉαž„αž’αžΆαž…αžŠαŸ†αžŽαžΎαžšαž€αžΆαžšαž…αŸ’αž”αžΆαž”αŸ‹αž‡αžΆαž…αŸ’αžšαžΎαž“ αž’αŸ’αžœαžΎαž€αžΆαžšαž€αŸ’αž“αž»αž„αžαŸ’αžŸαŸ‚αžŸαŸ’αžšαž‘αžΆαž™αž…αŸ’αžšαžΎαž“ αžŠαŸ„αž™αž―αž€αžšαžΆαž‡αŸ’αž™ αžαŸ’αžšαž½αžαž–αž·αž“αž·αžαŸ’αž™αž€αžΆαžšαž•αŸ’αž›αžΆαžŸαŸ‹αž”αŸ’αžαžΌαžšαž“αŸ…αž€αŸ’αž“αž»αž„ CRDs αžšαž”αžŸαŸ‹αžœαžΆ...

αžŠαžΎαž˜αŸ’αž”αžΈβ€‹αž±αŸ’αž™β€‹αž’αŸ’αž“αž€β€‹αž˜αžΎαž›β€‹αž€αžΆαž“αŸ‹αžαŸ‚β€‹αž…αŸ’αž”αžΆαžŸαŸ‹β€‹αž’αŸ†αž–αžΈβ€‹αž€αžΌαžŠβ€‹αž“αŸ„αŸ‡ αž™αžΎαž„β€‹αž”αžΆαž“β€‹αžŠαžΆαž€αŸ‹β€‹αžœαžΆβ€‹αž…αžΌαž› αžƒαŸ’αž›αžΆαŸ†αž„αžŸαžΆαž’αžΆαžšαžŽαŸˆ. αž”αŸ’αžšαžŸαž·αž“αž”αžΎαž’αŸ’αž“αž€αž…αž„αŸ‹αž”αžΆαž“αž§αž‘αžΆαž αžšαžŽαŸαž“αŸƒαž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαžŠαŸαž’αŸ’αž„αž“αŸ‹αž’αŸ’αž„αžšαž”αž“αŸ’αžαŸ‚αž˜αž‘αŸ€αžαžŠαŸ‚αž›αžαŸ’αžšαžΌαžœαž”αžΆαž“αž’αž“αž»αžœαžαŸ’αžαžŠαŸ„αž™αž”αŸ’αžšαžΎ Python αž’αŸ’αž“αž€αž’αžΆαž…αž”αž„αŸ’αžœαŸ‚αžšαž€αžΆαžšαž™αž€αž…αž·αžαŸ’αžαž‘αž»αž€αžŠαžΆαž€αŸ‹αžšαž”αžŸαŸ‹αž’αŸ’αž“αž€αž‘αŸ…αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαž–αžΈαžšαžŸαž˜αŸ’αžšαžΆαž”αŸ‹αžŠαžΆαž€αŸ‹αž–αž„αŸ’αžšαžΆαž™ mongodb (ΠΏΠ΅Ρ€Π²Ρ‹ΠΉ ΠΈ αž‘αžΈαž–αžΈαžš).

PS αž αžΎαž™αž”αŸ’αžšαžŸαž·αž“αž”αžΎαž’αŸ’αž“αž€αžαŸ’αž‡αž·αž›αž–αŸαž€αž€αŸ’αž“αž»αž„αž€αžΆαžšαžŠαŸ„αŸ‡αžŸαŸ’αžšαžΆαž™αž‡αžΆαž˜αž½αž™αž–αŸ’αžšαžΉαžαŸ’αžαž·αž€αžΆαžšαžŽαŸ Kubernetes αž¬αž’αŸ’αž“αž€αž€αžΆαž“αŸ‹αžαŸ‚αžŸαŸŠαžΆαŸ†αž“αžΉαž„αž€αžΆαžšαž”αŸ’αžšαžΎαž”αŸ’αžšαžΆαžŸαŸ‹ Bash αžŸαž αž€αžΆαžšαžΈαžšαž”αžŸαŸ‹αž™αžΎαž„αž”αžΆαž“αžšαŸ€αž”αž…αŸ†αžŠαŸ†αžŽαŸ„αŸ‡αžŸαŸ’αžšαžΆαž™αžŠαŸ‚αž›αžαŸ’αžšαŸ€αž˜αžšαž½αž…αž‡αžΆαžŸαŸ’αžšαŸαž…αž€αŸ’αž“αž»αž„αž‘αž˜αŸ’αžšαž„αŸ‹ αž”αŸ’αžšαžαž·αž”αžαŸ’αžαž·αž€αžšαžŸαŸ‚αž› (αž™αžΎαž„ αž”αžΆαž“αž”αŸ’αžšαž€αžΆαžŸ αžœαžΆαž“αŸ…αž€αŸ’αž“αž»αž„αžαŸ‚αž˜αŸαžŸαžΆ) αŸ”

PPS

αžŸαžΌαž˜αž’αžΆαž“αž•αž„αžŠαŸ‚αžšαž“αŸ…αž›αžΎαž”αŸ’αž›αž€αŸ‹αžšαž”αžŸαŸ‹αž™αžΎαž„αŸ–

αž”αŸ’αžšαž—αž–: www.habr.com

αž”αž“αŸ’αžαŸ‚αž˜αž˜αžαž·αž™αŸ„αž”αž›αŸ‹