{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeFamilies #-} -- | -- Top-Level data types for B9 build artifacts. module B9.Artifact.Readable ( ArtifactGenerator (..), InstanceId (..), ArtifactTarget (..), CloudInitType (..), ArtifactAssembly (..), AssembledArtifact (..), AssemblyOutput (..), instanceIdKey, buildIdKey, buildDateKey, getAssemblyOutput, -- ** Re-exports ArtifactSource (..), getArtifactSourceFiles, ) where import B9.Artifact.Readable.Source import B9.DiskImages import B9.QCUtil import B9.Vm import Control.Parallel.Strategies import Data.Binary import Data.Data import Data.Hashable import Data.Semigroup as Sem import GHC.Generics (Generic) import System.FilePath ((<.>)) import Test.QuickCheck -- | Artifacts represent the things B9 can build. A generator specifies howto -- generate parameterized, multiple artifacts. The general structure is: -- -- @ -- Let [ ... bindings ... ] -- [ Sources -- [ ... list all input files ... ] -- [ Artifact ... -- , Artifact ... -- , Let [ ... ] [ ... ] -- ] -- ] -- @ -- -- The reasons why 'Sources' takes a list of 'ArtifactGenerator's is that -- -- 1. this makes the value easier to read/write for humans -- -- 2. the sources are static files used in all children (e.g. company logo image) -- -- 3. the sources are parameterized by variables that bound to different values -- for each artifact, e.g. a template network config file which contains -- the host IP address. -- -- To bind such variables use 'Let', 'Each', 'LetX' or 'EachT'. -- -- String substitution of these variables is done by "B9.Artifact.Content.StringTemplate". -- These variables can be used as value in nested 'Let's, in most file names/paths -- and in source files added with 'B9.Artifact.Content.StringTemplate.SourceFile' -- -- -- @deprecated TODO remove this when switching to Dhall data ArtifactGenerator = -- | Add sources available to 'ArtifactAssembly's in -- nested artifact generators. Sources [ArtifactSource] [ArtifactGenerator] | -- | Bind variables, variables are available in nested -- generators. -- @deprecated TODO remove this when switching to Dhall Let [(String, String)] [ArtifactGenerator] | -- | A 'Let' where each variable is assigned to each -- value; the nested generator is executed for each -- permutation. -- -- @ -- LetX [("x", ["1","2","3"]), ("y", ["a","b"])] [..] -- @ -- Is equal to: -- -- @ -- Let [] [ -- Let [("x", "1"), ("y", "a")] [..] -- Let [("x", "1"), ("y", "b")] [..] -- Let [("x", "2"), ("y", "a")] [..] -- Let [("x", "2"), ("y", "b")] [..] -- Let [("x", "3"), ("y", "a")] [..] -- Let [("x", "3"), ("y", "b")] [..] -- ] -- @ -- @deprecated TODO remove this when switching to Dhall LetX [(String, [String])] [ArtifactGenerator] | -- | Bind each variable to their first value, then each -- variable to the second value, etc ... and execute the -- nested generator in every step. 'LetX' represents a -- product of all variables, whereas 'Each' represents a -- sum of variable bindings - 'Each' is more like a /zip/ -- whereas 'LetX' is more like a list comprehension. -- @deprecated TODO remove this when switching to Dhall Each [(String, [String])] [ArtifactGenerator] | -- | The transposed version of 'Each': Bind the variables -- in the first list to each a set of values from the -- second argument; execute the nested generators for -- each binding -- @deprecated TODO remove this when switching to Dhall EachT [String] [[String]] [ArtifactGenerator] | -- | Generate an artifact defined by an -- 'ArtifactAssembly'; the assembly can access the files -- created from the 'Sources' and variables bound by -- 'Let'ish elements. An artifact has an instance id, -- that is a unique, human readable string describing the -- artifact to assemble. Artifact InstanceId ArtifactAssembly | EmptyArtifact deriving (Read, Show, Eq, Data, Typeable, Generic) --instance Hashable ArtifactGenerator --instance Binary ArtifactGenerator instance NFData ArtifactGenerator instance Sem.Semigroup ArtifactGenerator where (Let [] []) <> x = x x <> (Let [] []) = x x <> y = Let [] [x, y] instance Monoid ArtifactGenerator where mempty = Let [] [] mappend = (Sem.<>) -- | Identify an artifact. __Deprecated__ TODO: B9 does not check if all -- instances IDs are unique. newtype InstanceId = IID String deriving (Read, Show, Typeable, Data, Eq, NFData, Binary, Hashable) -- | The variable containing the instance id. __Deprecated__ instanceIdKey :: String instanceIdKey = "instance_id" -- | The variable containing the buildId that identifies each execution of -- B9. For more info about variable substitution in source files see -- 'B9.Artifact.Content.StringTemplate' buildIdKey :: String buildIdKey = "build_id" -- | The variable containing the date and time a build was started. For more -- info about variable substitution in source files see -- 'B9.Artifact.Content.StringTemplate' buildDateKey :: String buildDateKey = "build_date" -- | Define an __output__ of a build. Assemblies are nested into -- 'ArtifactGenerator's. They contain all the files defined by the 'Sources' -- they are nested into. data ArtifactAssembly = -- | Generate a __cloud-init__ compatible directory, ISO- -- or VFAT image, as specified by the list of -- 'CloudInitType's. Every item will use the second -- argument to create an appropriate /file name/, -- e.g. for the 'CI_ISO' type the output is @second_param.iso@. CloudInit [CloudInitType] FilePath | -- | a set of VM-images that were created by executing a -- build script on them. VmImages [ImageTarget] VmScript deriving (Read, Show, Typeable, Data, Eq, Generic) instance Hashable ArtifactAssembly instance Binary ArtifactAssembly instance NFData ArtifactAssembly -- | A symbolic representation of the targets assembled by -- 'B9.Artifact.Readable.Interpreter.assemble' from an 'ArtifactAssembly'. There is a -- list of 'ArtifactTarget's because e.g. a single 'CloudInit' can produce up to -- three output files, a directory, an ISO image and a VFAT image. data AssembledArtifact = AssembledArtifact InstanceId [ArtifactTarget] deriving (Read, Show, Typeable, Data, Eq, Generic) instance Hashable AssembledArtifact instance Binary AssembledArtifact instance NFData AssembledArtifact data ArtifactTarget = CloudInitTarget CloudInitType FilePath | VmImagesTarget deriving (Read, Show, Typeable, Data, Eq, Generic) instance Hashable ArtifactTarget instance Binary ArtifactTarget instance NFData ArtifactTarget data CloudInitType = CI_ISO | CI_VFAT | CI_DIR deriving (Read, Show, Typeable, Data, Eq, Generic) instance Hashable CloudInitType instance Binary CloudInitType instance NFData CloudInitType -- | The output of an 'ArtifactAssembly' is either a set of generated files, -- or it might be a directory that contains the artifacts sources. data AssemblyOutput = AssemblyGeneratesOutputFiles [FilePath] | AssemblyCopiesSourcesToDirectory FilePath deriving (Read, Show, Typeable, Data, Eq, Generic) -- | Return the files that the artifact assembly consist of. getAssemblyOutput :: ArtifactAssembly -> [AssemblyOutput] getAssemblyOutput (VmImages ts _) = AssemblyGeneratesOutputFiles . getImageDestinationOutputFiles <$> ts getAssemblyOutput (CloudInit ts o) = getCloudInitOutputFiles o <$> ts where getCloudInitOutputFiles baseName t = case t of CI_ISO -> AssemblyGeneratesOutputFiles [baseName <.> "iso"] CI_VFAT -> AssemblyGeneratesOutputFiles [baseName <.> "vfat"] CI_DIR -> AssemblyCopiesSourcesToDirectory baseName -- * QuickCheck instances instance Arbitrary ArtifactGenerator where arbitrary = oneof [ Sources <$> halfSize arbitrary <*> halfSize arbitrary, Let <$> halfSize arbitraryEnv <*> halfSize arbitrary, halfSize arbitraryEachT <*> halfSize arbitrary, halfSize arbitraryEach <*> halfSize arbitrary, Artifact <$> smaller arbitrary <*> smaller arbitrary, pure EmptyArtifact ] arbitraryEachT :: Gen ([ArtifactGenerator] -> ArtifactGenerator) arbitraryEachT = sized $ \n -> EachT <$> vectorOf n (halfSize (listOf1 (choose ('a', 'z')))) <*> oneof [ listOf (vectorOf n (halfSize arbitrary)), listOf1 (listOf (halfSize arbitrary)) ] arbitraryEach :: Gen ([ArtifactGenerator] -> ArtifactGenerator) arbitraryEach = sized $ \n -> Each <$> listOf ( (,) <$> listOf1 (choose ('a', 'z')) <*> vectorOf n (halfSize (listOf1 (choose ('a', 'z')))) ) instance Arbitrary InstanceId where arbitrary = IID <$> arbitraryFilePath instance Arbitrary ArtifactAssembly where arbitrary = oneof [ CloudInit <$> arbitrary <*> arbitraryFilePath, VmImages <$> smaller arbitrary <*> pure NoVmScript ] instance Arbitrary CloudInitType where arbitrary = elements [CI_ISO, CI_VFAT, CI_DIR]